Compare commits
13 Commits
9cf41a3b7e
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5a65bab5b | ||
|
|
cb8b9da12e | ||
|
|
8cb67f1762 | ||
|
|
b9fc1ee05a | ||
|
|
93c63fd600 | ||
|
|
7dd0969799 | ||
|
|
a5fd9538fe | ||
|
|
7309fcc72d | ||
|
|
c26098b0d4 | ||
|
|
bfb174bcf8 | ||
|
|
0443bd251f | ||
|
|
56d0aad6a6 | ||
|
|
c0b1863467 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -43,3 +43,9 @@ nb-configuration.xml
|
||||
/.quarkus/cli/plugins/
|
||||
# TLS Certificates
|
||||
.certs/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
hs_err_pid*.log
|
||||
replay_pid*.log
|
||||
backend_log.txt
|
||||
|
||||
3
.mvn/jvm.config
Normal file
3
.mvn/jvm.config
Normal file
@@ -0,0 +1,3 @@
|
||||
-Xmx2048m
|
||||
-Xms1024m
|
||||
-XX:MaxMetaspaceSize=512m
|
||||
450
REALTIME_ARCHITECTURE_BRAINSTORM.md
Normal file
450
REALTIME_ARCHITECTURE_BRAINSTORM.md
Normal file
@@ -0,0 +1,450 @@
|
||||
# 🚀 Architecture Temps Réel - Brainstorming & Plan d'Implémentation
|
||||
|
||||
## 📋 Contexte Actuel
|
||||
|
||||
### État des Lieux
|
||||
- ✅ **Backend**: Jakarta WebSocket (`@ServerEndpoint`) - API legacy
|
||||
- ✅ **Frontend**: `web_socket_channel` pour WebSocket
|
||||
- ✅ **Services existants**:
|
||||
- `NotificationWebSocket` (`/notifications/ws/{userId}`)
|
||||
- `ChatWebSocket` (`/chat/ws/{userId}`)
|
||||
- Services Flutter: `RealtimeNotificationService`, `ChatWebSocketService`
|
||||
|
||||
### Limitations Actuelles
|
||||
1. **Pas de persistance des événements** : Si un utilisateur est déconnecté, les messages sont perdus
|
||||
2. **Pas de scalabilité horizontale** : Les sessions WebSocket sont en mémoire, ne fonctionnent pas avec plusieurs instances
|
||||
3. **Pas de garantie de livraison** : Pas de mécanisme de retry ou de queue
|
||||
4. **Pas de découplage** : Services directement couplés aux WebSockets
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Objectifs
|
||||
|
||||
1. **Garantir la livraison** : Aucun événement ne doit être perdu
|
||||
2. **Scalabilité horizontale** : Support de plusieurs instances Quarkus
|
||||
3. **Temps réel garanti** : Latence < 100ms pour les notifications critiques
|
||||
4. **Durabilité** : Persistance des événements pour récupération après déconnexion
|
||||
5. **Découplage** : Services métier indépendants des WebSockets
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Proposée
|
||||
|
||||
### Option 1 : WebSockets Next + Kafka (Recommandée) ⭐
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Flutter │
|
||||
│ Client │
|
||||
└──────┬──────┘
|
||||
│ WebSocket (wss://)
|
||||
│
|
||||
┌──────▼─────────────────────────────────────┐
|
||||
│ Quarkus WebSockets Next │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ @WebSocket("/notifications/{userId}")│ │
|
||||
│ │ @WebSocket("/chat/{userId}") │ │
|
||||
│ └──────┬───────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────▼──────────────────────────────┐ │
|
||||
│ │ Reactive Messaging Bridge │ │
|
||||
│ │ @Incoming("kafka-notifications") │ │
|
||||
│ │ @Outgoing("websocket-notifications") │ │
|
||||
│ └──────┬───────────────────────────────┘ │
|
||||
└─────────┼──────────────────────────────────┘
|
||||
│
|
||||
│ Kafka Topics
|
||||
│
|
||||
┌─────────▼──────────────────────────────────┐
|
||||
│ Apache Kafka Cluster │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ Topics: │ │
|
||||
│ │ - notifications.{userId} │ │
|
||||
│ │ - chat.messages │ │
|
||||
│ │ - reactions.{postId} │ │
|
||||
│ │ - presence.updates │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└─────────┬──────────────────────────────────┘
|
||||
│
|
||||
│ Producers
|
||||
│
|
||||
┌─────────▼──────────────────────────────────┐
|
||||
│ Services Métier (Quarkus) │
|
||||
│ - FriendshipService │
|
||||
│ - MessageService │
|
||||
│ - SocialPostService │
|
||||
│ - EventService │
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Avantages
|
||||
- ✅ **Scalabilité** : Kafka gère la distribution entre instances
|
||||
- ✅ **Durabilité** : Messages persistés dans Kafka (rétention configurable)
|
||||
- ✅ **Découplage** : Services publient dans Kafka, WebSocket consomme
|
||||
- ✅ **Performance** : WebSockets Next est plus performant que Jakarta WS
|
||||
- ✅ **Replay** : Possibilité de rejouer les événements pour récupération
|
||||
- ✅ **Monitoring** : Kafka fournit des métriques natives
|
||||
|
||||
### Inconvénients
|
||||
- ⚠️ **Complexité** : Nécessite un cluster Kafka (mais Quarkus Dev Services le gère automatiquement)
|
||||
- ⚠️ **Latence** : Légèrement plus élevée (Kafka + WebSocket vs WebSocket direct)
|
||||
- ⚠️ **Ressources** : Kafka consomme plus de mémoire
|
||||
|
||||
---
|
||||
|
||||
## 📦 Technologies & Dépendances
|
||||
|
||||
### Backend (Quarkus)
|
||||
|
||||
#### 1. WebSockets Next (Remplace Jakarta WebSocket)
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-websockets-next</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**Documentation**: https://quarkus.io/guides/websockets-next
|
||||
|
||||
#### 2. Kafka Reactive Messaging
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-messaging-kafka</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**Documentation**: https://quarkus.io/guides/kafka
|
||||
|
||||
#### 3. Reactive Messaging HTTP (Bridge Kafka ↔ WebSocket)
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.quarkiverse.reactivemessaginghttp</groupId>
|
||||
<artifactId>quarkus-reactive-messaging-http</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**Documentation**: https://docs.quarkiverse.io/quarkus-reactive-messaging-http/dev/reactive-messaging-websocket.html
|
||||
|
||||
### Frontend (Flutter)
|
||||
|
||||
#### Package WebSocket (Déjà utilisé)
|
||||
```yaml
|
||||
dependencies:
|
||||
web_socket_channel: ^2.4.0
|
||||
```
|
||||
|
||||
**Documentation**: https://pub.dev/packages/web_socket_channel
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### application.properties (Quarkus)
|
||||
|
||||
```properties
|
||||
# ============================================
|
||||
# Kafka Configuration
|
||||
# ============================================
|
||||
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
|
||||
|
||||
# Topics
|
||||
mp.messaging.outgoing.notifications.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.notifications.topic=notifications
|
||||
mp.messaging.outgoing.notifications.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.notifications.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||
|
||||
mp.messaging.outgoing.chat-messages.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.chat-messages.topic=chat.messages
|
||||
mp.messaging.outgoing.chat-messages.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.chat-messages.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||
|
||||
mp.messaging.outgoing.reactions.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.reactions.topic=reactions
|
||||
mp.messaging.outgoing.reactions.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.reactions.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||
|
||||
# ============================================
|
||||
# WebSocket Configuration
|
||||
# ============================================
|
||||
# WebSockets Next
|
||||
quarkus.websockets-next.server.enabled=true
|
||||
quarkus.websockets-next.server.port=8080
|
||||
|
||||
# ============================================
|
||||
# Reactive Messaging HTTP (Bridge)
|
||||
# ============================================
|
||||
# Incoming Kafka → Outgoing WebSocket
|
||||
mp.messaging.incoming.kafka-notifications.connector=smallrye-kafka
|
||||
mp.messaging.incoming.kafka-notifications.topic=notifications
|
||||
mp.messaging.incoming.kafka-notifications.group.id=websocket-bridge
|
||||
mp.messaging.incoming.kafka-notifications.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.kafka-notifications.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
|
||||
|
||||
mp.messaging.outgoing.ws-notifications.connector=quarkus-websocket
|
||||
mp.messaging.outgoing.ws-notifications.path=/notifications/{userId}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 Implémentation
|
||||
|
||||
### Backend : Migration vers WebSockets Next
|
||||
|
||||
#### 1. Nouveau NotificationWebSocket (WebSockets Next)
|
||||
|
||||
```java
|
||||
package com.lions.dev.websocket;
|
||||
|
||||
import io.quarkus.websockets.next.OnOpen;
|
||||
import io.quarkus.websockets.next.OnClose;
|
||||
import io.quarkus.websockets.next.OnTextMessage;
|
||||
import io.quarkus.websockets.next.WebSocket;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import io.smallrye.mutiny.Multi;
|
||||
import jakarta.inject.Inject;
|
||||
import org.eclipse.microprofile.reactive.messaging.Channel;
|
||||
import org.eclipse.microprofile.reactive.messaging.Emitter;
|
||||
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
||||
import org.eclipse.microprofile.reactive.messaging.Outgoing;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* WebSocket endpoint pour les notifications en temps réel (WebSockets Next).
|
||||
*
|
||||
* Architecture:
|
||||
* - Services métier → Kafka Topic → WebSocket Bridge → Client
|
||||
*/
|
||||
@WebSocket(path = "/notifications/{userId}")
|
||||
public class NotificationWebSocketNext {
|
||||
|
||||
@Inject
|
||||
@Channel("ws-notifications")
|
||||
Emitter<String> notificationEmitter;
|
||||
|
||||
// Stockage des connexions actives (pour routing)
|
||||
private static final Map<UUID, WebSocketConnection> connections = new ConcurrentHashMap<>();
|
||||
|
||||
@OnOpen
|
||||
public void onOpen(WebSocketConnection connection, String userId) {
|
||||
UUID userUUID = UUID.fromString(userId);
|
||||
connections.put(userUUID, connection);
|
||||
|
||||
// Envoyer confirmation de connexion
|
||||
connection.sendText("{\"type\":\"connected\",\"timestamp\":" + System.currentTimeMillis() + "}");
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(String userId) {
|
||||
UUID userUUID = UUID.fromString(userId);
|
||||
connections.remove(userUUID);
|
||||
}
|
||||
|
||||
@OnTextMessage
|
||||
public void onMessage(String message, String userId) {
|
||||
// Gérer les messages du client (ping, ack, etc.)
|
||||
// ...
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge: Consomme depuis Kafka et envoie via WebSocket
|
||||
*/
|
||||
@Incoming("kafka-notifications")
|
||||
@Outgoing("ws-notifications")
|
||||
public Multi<String> bridgeNotifications(String kafkaMessage) {
|
||||
// Parser le message Kafka
|
||||
// Extraire userId depuis la clé Kafka
|
||||
// Router vers la bonne connexion WebSocket
|
||||
return Multi.createFrom().item(kafkaMessage);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Service Métier Publie dans Kafka
|
||||
|
||||
```java
|
||||
package com.lions.dev.service;
|
||||
|
||||
import org.eclipse.microprofile.reactive.messaging.Channel;
|
||||
import org.eclipse.microprofile.reactive.messaging.Emitter;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
@ApplicationScoped
|
||||
public class FriendshipService {
|
||||
|
||||
@Inject
|
||||
@Channel("notifications")
|
||||
Emitter<NotificationEvent> notificationEmitter;
|
||||
|
||||
public void sendFriendRequest(UUID fromUserId, UUID toUserId) {
|
||||
// ... logique métier ...
|
||||
|
||||
// Publier dans Kafka au lieu d'appeler directement WebSocket
|
||||
NotificationEvent event = new NotificationEvent(
|
||||
toUserId.toString(),
|
||||
"friend_request",
|
||||
Map.of("fromUserId", fromUserId.toString(), "fromName", fromUser.getFirstName())
|
||||
);
|
||||
|
||||
notificationEmitter.send(event);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend : Amélioration du Service WebSocket
|
||||
|
||||
```dart
|
||||
// afterwork/lib/data/services/realtime_notification_service_v2.dart
|
||||
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:web_socket_channel/status.dart' as status;
|
||||
|
||||
class RealtimeNotificationServiceV2 {
|
||||
WebSocketChannel? _channel;
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _reconnectTimer;
|
||||
int _reconnectAttempts = 0;
|
||||
static const int _maxReconnectAttempts = 5;
|
||||
static const Duration _heartbeatInterval = Duration(seconds: 30);
|
||||
static const Duration _reconnectDelay = Duration(seconds: 5);
|
||||
|
||||
Future<void> connect(String userId, String authToken) async {
|
||||
final uri = Uri.parse('wss://api.afterwork.lions.dev/notifications/$userId');
|
||||
|
||||
_channel = WebSocketChannel.connect(
|
||||
uri,
|
||||
protocols: ['notifications-v2'],
|
||||
headers: {
|
||||
'Authorization': 'Bearer $authToken',
|
||||
},
|
||||
);
|
||||
|
||||
// Heartbeat pour maintenir la connexion
|
||||
_heartbeatTimer = Timer.periodic(_heartbeatInterval, (_) {
|
||||
_channel?.sink.add(jsonEncode({'type': 'ping'}));
|
||||
});
|
||||
|
||||
// Écouter les messages
|
||||
_channel!.stream.listen(
|
||||
_handleMessage,
|
||||
onError: _handleError,
|
||||
onDone: _handleDisconnection,
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDisconnection() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_scheduleReconnect();
|
||||
}
|
||||
|
||||
void _scheduleReconnect() {
|
||||
if (_reconnectAttempts < _maxReconnectAttempts) {
|
||||
_reconnectTimer = Timer(_reconnectDelay * (_reconnectAttempts + 1), () {
|
||||
_reconnectAttempts++;
|
||||
connect(_userId, _authToken);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Comparaison des Options
|
||||
|
||||
| Critère | Jakarta WS (Actuel) | WebSockets Next | WebSockets Next + Kafka |
|
||||
|---------|---------------------|-----------------|-------------------------|
|
||||
| **Performance** | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| **Scalabilité** | ❌ (1 instance) | ⚠️ (limité) | ✅ (illimitée) |
|
||||
| **Durabilité** | ❌ | ❌ | ✅ |
|
||||
| **Découplage** | ❌ | ⚠️ | ✅ |
|
||||
| **Complexité** | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| **Latence** | < 50ms | < 50ms | < 100ms |
|
||||
| **Replay Events** | ❌ | ❌ | ✅ |
|
||||
| **Monitoring** | ⚠️ | ⚠️ | ✅ (Kafka metrics) |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Plan d'Implémentation Recommandé
|
||||
|
||||
### Phase 1 : Migration WebSockets Next (Sans Kafka)
|
||||
**Durée**: 1-2 semaines
|
||||
- Migrer `NotificationWebSocket` vers WebSockets Next
|
||||
- Migrer `ChatWebSocket` vers WebSockets Next
|
||||
- Tester avec le frontend existant
|
||||
- **Avantage**: Amélioration immédiate des performances
|
||||
|
||||
### Phase 2 : Intégration Kafka
|
||||
**Durée**: 2-3 semaines
|
||||
- Ajouter dépendances Kafka
|
||||
- Créer les topics Kafka
|
||||
- Implémenter les bridges Kafka ↔ WebSocket
|
||||
- Migrer les services pour publier dans Kafka
|
||||
- **Avantage**: Scalabilité et durabilité
|
||||
|
||||
### Phase 3 : Optimisations
|
||||
**Durée**: 1 semaine
|
||||
- Compression des messages
|
||||
- Batching pour les notifications
|
||||
- Monitoring et alertes
|
||||
- **Avantage**: Performance et observabilité
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Alternatives Considérées
|
||||
|
||||
### Option 2 : Server-Sent Events (SSE)
|
||||
- ✅ Plus simple que WebSocket
|
||||
- ❌ Unidirectionnel (serveur → client uniquement)
|
||||
- ❌ Pas adapté pour le chat bidirectionnel
|
||||
|
||||
### Option 3 : gRPC Streaming
|
||||
- ✅ Performant
|
||||
- ❌ Plus complexe à configurer
|
||||
- ❌ Nécessite HTTP/2
|
||||
|
||||
### Option 4 : Socket.IO
|
||||
- ✅ Reconnexion automatique
|
||||
- ❌ Nécessite Node.js (pas compatible Quarkus)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Ressources & Documentation
|
||||
|
||||
### Quarkus
|
||||
- [WebSockets Next Guide](https://quarkus.io/guides/websockets-next)
|
||||
- [Kafka Guide](https://quarkus.io/guides/kafka)
|
||||
- [Reactive Messaging](https://quarkus.io/guides/messaging)
|
||||
|
||||
### Kafka
|
||||
- [Kafka Documentation](https://kafka.apache.org/documentation/)
|
||||
- [Quarkus Kafka Dev Services](https://quarkus.io/guides/kafka-dev-services)
|
||||
|
||||
### Flutter
|
||||
- [web_socket_channel Package](https://pub.dev/packages/web_socket_channel)
|
||||
- [Flutter WebSocket Best Practices](https://ably.com/topic/websockets-flutter)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Recommandation Finale
|
||||
|
||||
**Architecture recommandée**: **WebSockets Next + Kafka**
|
||||
|
||||
**Raisons**:
|
||||
1. ✅ Scalabilité horizontale garantie
|
||||
2. ✅ Durabilité des événements
|
||||
3. ✅ Découplage des services
|
||||
4. ✅ Compatible avec l'existant (migration progressive)
|
||||
5. ✅ Support natif Quarkus (Dev Services pour Kafka)
|
||||
6. ✅ Monitoring intégré
|
||||
|
||||
**Prochaines étapes**:
|
||||
1. Ajouter les dépendances dans `pom.xml`
|
||||
2. Créer un POC avec un seul endpoint (notifications)
|
||||
3. Tester la scalabilité avec 2 instances Quarkus
|
||||
4. Migrer progressivement les autres endpoints
|
||||
930
REALTIME_IMPLEMENTATION_EXAMPLES.md
Normal file
930
REALTIME_IMPLEMENTATION_EXAMPLES.md
Normal file
@@ -0,0 +1,930 @@
|
||||
# 💻 Exemples d'Implémentation - Temps Réel avec Kafka
|
||||
|
||||
## 📦 Étape 1 : Ajouter les Dépendances
|
||||
|
||||
### pom.xml
|
||||
|
||||
```xml
|
||||
<!-- WebSockets Next (remplace quarkus-websockets) -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-websockets-next</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Kafka Reactive Messaging -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-messaging-kafka</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Reactive Messaging HTTP (Bridge Kafka ↔ WebSocket) -->
|
||||
<dependency>
|
||||
<groupId>io.quarkiverse.reactivemessaginghttp</groupId>
|
||||
<artifactId>quarkus-reactive-messaging-http</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Serialization pour Kafka -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jsonb</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Étape 2 : Configuration application.properties
|
||||
|
||||
```properties
|
||||
# ============================================
|
||||
# Kafka Configuration
|
||||
# ============================================
|
||||
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
|
||||
|
||||
# Topic: Notifications
|
||||
mp.messaging.outgoing.notifications.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.notifications.topic=notifications
|
||||
mp.messaging.outgoing.notifications.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.notifications.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||
|
||||
# Topic: Chat Messages
|
||||
mp.messaging.outgoing.chat-messages.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.chat-messages.topic=chat.messages
|
||||
mp.messaging.outgoing.chat-messages.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.chat-messages.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||
|
||||
# Topic: Reactions (likes, comments)
|
||||
mp.messaging.outgoing.reactions.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.reactions.topic=reactions
|
||||
mp.messaging.outgoing.reactions.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.reactions.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||
|
||||
# Topic: Presence Updates
|
||||
mp.messaging.outgoing.presence.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.presence.topic=presence.updates
|
||||
mp.messaging.outgoing.presence.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.presence.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||
|
||||
# ============================================
|
||||
# Kafka → WebSocket Bridge (Incoming)
|
||||
# ============================================
|
||||
# Consommer depuis Kafka et router vers WebSocket
|
||||
mp.messaging.incoming.kafka-notifications.connector=smallrye-kafka
|
||||
mp.messaging.incoming.kafka-notifications.topic=notifications
|
||||
mp.messaging.incoming.kafka-notifications.group.id=websocket-notifications-bridge
|
||||
mp.messaging.incoming.kafka-notifications.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.kafka-notifications.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
|
||||
mp.messaging.incoming.kafka-notifications.enable.auto.commit=true
|
||||
|
||||
mp.messaging.incoming.kafka-chat.connector=smallrye-kafka
|
||||
mp.messaging.incoming.kafka-chat.topic=chat.messages
|
||||
mp.messaging.incoming.kafka-chat.group.id=websocket-chat-bridge
|
||||
mp.messaging.incoming.kafka-chat.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.kafka-chat.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
|
||||
mp.messaging.incoming.kafka-chat.enable.auto.commit=true
|
||||
|
||||
# ============================================
|
||||
# WebSocket Configuration
|
||||
# ============================================
|
||||
quarkus.websockets-next.server.enabled=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Étape 3 : DTOs pour les Événements Kafka
|
||||
|
||||
### NotificationEvent.java
|
||||
|
||||
```java
|
||||
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.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class NotificationEvent {
|
||||
private String userId; // Clé Kafka (pour routing)
|
||||
private String type; // friend_request, event_reminder, message_alert, etc.
|
||||
private Map<String, Object> data;
|
||||
private Long timestamp;
|
||||
|
||||
public NotificationEvent(String userId, String type, Map<String, Object> data) {
|
||||
this.userId = userId;
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ChatMessageEvent.java
|
||||
|
||||
```java
|
||||
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.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ChatMessageEvent {
|
||||
private String conversationId; // Clé Kafka
|
||||
private String senderId;
|
||||
private String recipientId;
|
||||
private String content;
|
||||
private String messageId;
|
||||
private Long timestamp;
|
||||
}
|
||||
```
|
||||
|
||||
### ReactionEvent.java
|
||||
|
||||
```java
|
||||
package com.lions.dev.dto.events;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Événement de réaction (like, comment) publié dans Kafka.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ReactionEvent {
|
||||
private String postId; // Clé Kafka
|
||||
private String userId;
|
||||
private String reactionType; // like, comment, share
|
||||
private Map<String, Object> data;
|
||||
private Long timestamp;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Étape 4 : WebSocket avec WebSockets Next
|
||||
|
||||
### NotificationWebSocketNext.java
|
||||
|
||||
```java
|
||||
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 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:
|
||||
* Services → Kafka → Bridge → WebSocket → Client
|
||||
*/
|
||||
@WebSocket(path = "/notifications/{userId}")
|
||||
@ApplicationScoped
|
||||
public class NotificationWebSocketNext {
|
||||
|
||||
// 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) {
|
||||
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
|
||||
connection.sendText("{\"type\":\"connected\",\"timestamp\":" +
|
||||
System.currentTimeMillis() + "}");
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.error("[WS-NEXT] UUID invalide: " + userId, e);
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(String userId) {
|
||||
try {
|
||||
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);
|
||||
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, String userId) {
|
||||
try {
|
||||
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) {
|
||||
UUID userUUID = UUID.fromString(userId);
|
||||
Set<WebSocketConnection> connections = userConnections.get(userUUID);
|
||||
|
||||
if (connections != null) {
|
||||
String pong = "{\"type\":\"pong\",\"timestamp\":" +
|
||||
System.currentTimeMillis() + "}";
|
||||
connections.forEach(conn -> {
|
||||
if (conn.isOpen()) {
|
||||
conn.sendText(pong);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAck(Map<String, Object> messageData, String userId) {
|
||||
String notificationId = (String) messageData.get("notificationId");
|
||||
Log.debug("[WS-NEXT] ACK reçu pour notification " + notificationId +
|
||||
" de " + userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une notification à un utilisateur spécifique.
|
||||
* Appelé par le bridge Kafka → WebSocket.
|
||||
*/
|
||||
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 + ")");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌉 Étape 5 : Bridge Kafka → WebSocket
|
||||
|
||||
### NotificationKafkaBridge.java
|
||||
|
||||
```java
|
||||
package com.lions.dev.websocket;
|
||||
|
||||
import com.lions.dev.dto.events.NotificationEvent;
|
||||
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.
|
||||
*
|
||||
* Architecture:
|
||||
* Kafka Topic (notifications) → Bridge → WebSocket (NotificationWebSocketNext)
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class NotificationKafkaBridge {
|
||||
|
||||
/**
|
||||
* Consomme les événements depuis Kafka et les route vers WebSocket.
|
||||
*/
|
||||
@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();
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.error("[KAFKA-BRIDGE] Erreur traitement événement", e);
|
||||
message.nack(e);
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
"timestamp", event.getTimestamp()
|
||||
);
|
||||
|
||||
return mapper.writeValueAsString(wsMessage);
|
||||
} catch (Exception e) {
|
||||
Log.error("[KAFKA-BRIDGE] Erreur construction message", e);
|
||||
return "{\"type\":\"error\",\"message\":\"Erreur de traitement\"}";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📤 Étape 6 : Services Publient dans Kafka
|
||||
|
||||
### FriendshipService (Modifié)
|
||||
|
||||
```java
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.dto.events.NotificationEvent;
|
||||
import org.eclipse.microprofile.reactive.messaging.Channel;
|
||||
import org.eclipse.microprofile.reactive.messaging.Emitter;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@ApplicationScoped
|
||||
public class FriendshipService {
|
||||
|
||||
@Inject
|
||||
@Channel("notifications")
|
||||
Emitter<NotificationEvent> notificationEmitter;
|
||||
|
||||
// ... autres dépendances ...
|
||||
|
||||
/**
|
||||
* Envoie une demande d'amitié (publie dans Kafka).
|
||||
*/
|
||||
@Transactional
|
||||
public FriendshipCreateOneResponseDTO sendFriendRequest(
|
||||
FriendshipCreateOneRequestDTO request) {
|
||||
|
||||
// ... logique métier existante ...
|
||||
|
||||
// ✅ NOUVEAU: Publier dans Kafka au lieu d'appeler directement WebSocket
|
||||
try {
|
||||
NotificationEvent event = new NotificationEvent(
|
||||
request.getFriendId().toString(), // userId destinataire
|
||||
"friend_request",
|
||||
java.util.Map.of(
|
||||
"fromUserId", request.getUserId().toString(),
|
||||
"fromFirstName", user.getFirstName(),
|
||||
"fromLastName", user.getLastName(),
|
||||
"requestId", response.getFriendshipId().toString()
|
||||
)
|
||||
);
|
||||
|
||||
notificationEmitter.send(event);
|
||||
logger.info("[LOG] Événement friend_request publié dans Kafka pour: " +
|
||||
request.getFriendId());
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("[ERROR] Erreur publication Kafka", e);
|
||||
// Ne pas bloquer la demande d'amitié si Kafka échoue
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepte une demande d'amitié (publie dans Kafka).
|
||||
*/
|
||||
@Transactional
|
||||
public FriendshipCreateOneResponseDTO acceptFriendRequest(UUID friendshipId) {
|
||||
// ... logique métier existante ...
|
||||
|
||||
// ✅ NOUVEAU: Publier dans Kafka
|
||||
try {
|
||||
NotificationEvent event = new NotificationEvent(
|
||||
originalRequest.getUserId().toString(), // userId émetteur
|
||||
"friend_request_accepted",
|
||||
java.util.Map.of(
|
||||
"friendId", friend.getId().toString(),
|
||||
"friendFirstName", friend.getFirstName(),
|
||||
"friendLastName", friend.getLastName(),
|
||||
"friendshipId", response.getFriendshipId().toString()
|
||||
)
|
||||
);
|
||||
|
||||
notificationEmitter.send(event);
|
||||
logger.info("[LOG] Événement friend_request_accepted publié dans Kafka");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("[ERROR] Erreur publication Kafka", e);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MessageService (Modifié)
|
||||
|
||||
```java
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.dto.events.ChatMessageEvent;
|
||||
import org.eclipse.microprofile.reactive.messaging.Channel;
|
||||
import org.eclipse.microprofile.reactive.messaging.Emitter;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
@ApplicationScoped
|
||||
public class MessageService {
|
||||
|
||||
@Inject
|
||||
@Channel("chat-messages")
|
||||
Emitter<ChatMessageEvent> chatMessageEmitter;
|
||||
|
||||
/**
|
||||
* Envoie un message (publie dans Kafka).
|
||||
*/
|
||||
@Transactional
|
||||
public MessageResponseDTO sendMessage(SendMessageRequestDTO request) {
|
||||
// ... logique métier existante ...
|
||||
|
||||
// ✅ NOUVEAU: Publier dans Kafka
|
||||
try {
|
||||
ChatMessageEvent event = new ChatMessageEvent();
|
||||
event.setConversationId(conversation.getId().toString());
|
||||
event.setSenderId(senderId.toString());
|
||||
event.setRecipientId(recipientId.toString());
|
||||
event.setContent(request.getContent());
|
||||
event.setMessageId(message.getId().toString());
|
||||
event.setTimestamp(System.currentTimeMillis());
|
||||
|
||||
// Utiliser conversationId comme clé Kafka pour garantir l'ordre
|
||||
chatMessageEmitter.send(org.eclipse.microprofile.reactive.messaging.Message.of(
|
||||
event,
|
||||
() -> CompletableFuture.completedFuture(null), // ack
|
||||
throwable -> {
|
||||
logger.error("[ERROR] Erreur envoi Kafka", throwable);
|
||||
return CompletableFuture.completedFuture(null); // nack
|
||||
}
|
||||
).addMetadata(org.eclipse.microprofile.reactive.messaging.OutgoingMessageMetadata.builder()
|
||||
.withKey(conversation.getId().toString())
|
||||
.build()));
|
||||
|
||||
logger.info("[LOG] Message publié dans Kafka: " + message.getId());
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("[ERROR] Erreur publication Kafka", e);
|
||||
// Ne pas bloquer l'envoi du message si Kafka échoue
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SocialPostService (Modifié pour les Réactions)
|
||||
|
||||
```java
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.dto.events.ReactionEvent;
|
||||
import org.eclipse.microprofile.reactive.messaging.Channel;
|
||||
import org.eclipse.microprofile.reactive.messaging.Emitter;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SocialPostService {
|
||||
|
||||
@Inject
|
||||
@Channel("reactions")
|
||||
Emitter<ReactionEvent> reactionEmitter;
|
||||
|
||||
/**
|
||||
* Like un post (publie dans Kafka).
|
||||
*/
|
||||
@Transactional
|
||||
public SocialPost likePost(UUID postId, UUID userId) {
|
||||
// ... logique métier existante ...
|
||||
|
||||
// ✅ NOUVEAU: Publier dans Kafka pour notifier en temps réel
|
||||
try {
|
||||
ReactionEvent event = new ReactionEvent();
|
||||
event.setPostId(postId.toString());
|
||||
event.setUserId(userId.toString());
|
||||
event.setReactionType("like");
|
||||
event.setData(java.util.Map.of(
|
||||
"postId", postId.toString(),
|
||||
"userId", userId.toString(),
|
||||
"likesCount", post.getLikesCount()
|
||||
));
|
||||
event.setTimestamp(System.currentTimeMillis());
|
||||
|
||||
reactionEmitter.send(event);
|
||||
logger.info("[LOG] Réaction like publiée dans Kafka pour post: " + postId);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("[ERROR] Erreur publication Kafka", e);
|
||||
}
|
||||
|
||||
return post;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Frontend : Amélioration du Service WebSocket
|
||||
|
||||
### realtime_notification_service_v2.dart
|
||||
|
||||
```dart
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:web_socket_channel/status.dart' as status;
|
||||
|
||||
class RealtimeNotificationServiceV2 extends ChangeNotifier {
|
||||
RealtimeNotificationServiceV2(this.userId, this.authToken);
|
||||
|
||||
final String userId;
|
||||
final String authToken;
|
||||
WebSocketChannel? _channel;
|
||||
StreamSubscription? _subscription;
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _reconnectTimer;
|
||||
|
||||
bool _isConnected = false;
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
int _reconnectAttempts = 0;
|
||||
static const int _maxReconnectAttempts = 5;
|
||||
static const Duration _heartbeatInterval = Duration(seconds: 30);
|
||||
static const Duration _reconnectDelay = Duration(seconds: 5);
|
||||
|
||||
// Streams pour différents types d'événements
|
||||
final _friendRequestController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _systemNotificationController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _reactionController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
|
||||
Stream<Map<String, dynamic>> get friendRequestStream => _friendRequestController.stream;
|
||||
Stream<Map<String, dynamic>> get systemNotificationStream => _systemNotificationController.stream;
|
||||
Stream<Map<String, dynamic>> get reactionStream => _reactionController.stream;
|
||||
|
||||
String get _wsUrl {
|
||||
final baseUrl = 'wss://api.afterwork.lions.dev'; // Production
|
||||
return '$baseUrl/notifications/$userId';
|
||||
}
|
||||
|
||||
Future<void> connect() async {
|
||||
if (_isConnected) return;
|
||||
|
||||
try {
|
||||
_channel = WebSocketChannel.connect(
|
||||
Uri.parse(_wsUrl),
|
||||
protocols: ['notifications-v2'],
|
||||
headers: {
|
||||
'Authorization': 'Bearer $authToken',
|
||||
},
|
||||
);
|
||||
|
||||
// Heartbeat pour maintenir la connexion
|
||||
_heartbeatTimer = Timer.periodic(_heartbeatInterval, (_) {
|
||||
_channel?.sink.add(jsonEncode({'type': 'ping'}));
|
||||
});
|
||||
|
||||
// Écouter les messages
|
||||
_subscription = _channel!.stream.listen(
|
||||
_handleMessage,
|
||||
onError: _handleError,
|
||||
onDone: _handleDisconnection,
|
||||
cancelOnError: false,
|
||||
);
|
||||
|
||||
_isConnected = true;
|
||||
notifyListeners();
|
||||
|
||||
} catch (e) {
|
||||
_isConnected = false;
|
||||
notifyListeners();
|
||||
_scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleMessage(dynamic message) {
|
||||
try {
|
||||
final data = jsonDecode(message as String) as Map<String, dynamic>;
|
||||
final type = data['type'] as String;
|
||||
|
||||
switch (type) {
|
||||
case 'connected':
|
||||
_reconnectAttempts = 0; // Reset sur reconnexion réussie
|
||||
break;
|
||||
case 'pong':
|
||||
// Heartbeat réponse
|
||||
break;
|
||||
case 'friend_request':
|
||||
case 'friend_request_accepted':
|
||||
_friendRequestController.add(data);
|
||||
break;
|
||||
case 'event_reminder':
|
||||
case 'system_notification':
|
||||
_systemNotificationController.add(data);
|
||||
break;
|
||||
case 'reaction':
|
||||
_reactionController.add(data);
|
||||
break;
|
||||
default:
|
||||
// Type inconnu, ignorer ou logger
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Erreur de parsing, ignorer
|
||||
}
|
||||
}
|
||||
|
||||
void _handleError(dynamic error) {
|
||||
_isConnected = false;
|
||||
notifyListeners();
|
||||
_scheduleReconnect();
|
||||
}
|
||||
|
||||
void _handleDisconnection() {
|
||||
_isConnected = false;
|
||||
notifyListeners();
|
||||
_scheduleReconnect();
|
||||
}
|
||||
|
||||
void _scheduleReconnect() {
|
||||
if (_reconnectAttempts >= _maxReconnectAttempts) {
|
||||
// Arrêter les tentatives après max
|
||||
return;
|
||||
}
|
||||
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = Timer(_reconnectDelay * (_reconnectAttempts + 1), () {
|
||||
_reconnectAttempts++;
|
||||
connect();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
_heartbeatTimer?.cancel();
|
||||
_reconnectTimer?.cancel();
|
||||
await _subscription?.cancel();
|
||||
await _channel?.sink.close(status.normalClosure);
|
||||
_isConnected = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disconnect();
|
||||
_friendRequestController.close();
|
||||
_systemNotificationController.close();
|
||||
_reactionController.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Tests
|
||||
|
||||
### Test du Bridge Kafka → WebSocket
|
||||
|
||||
```java
|
||||
package com.lions.dev.websocket;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.eclipse.microprofile.reactive.messaging.Channel;
|
||||
import org.eclipse.microprofile.reactive.messaging.Emitter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@QuarkusTest
|
||||
public class NotificationKafkaBridgeTest {
|
||||
|
||||
@Inject
|
||||
@Channel("notifications")
|
||||
Emitter<NotificationEvent> notificationEmitter;
|
||||
|
||||
@Test
|
||||
public void testNotificationFlow() {
|
||||
// Publier un événement dans Kafka
|
||||
NotificationEvent event = new NotificationEvent(
|
||||
"user-123",
|
||||
"friend_request",
|
||||
Map.of("fromUserId", "user-456")
|
||||
);
|
||||
|
||||
notificationEmitter.send(event);
|
||||
|
||||
// Vérifier que le message arrive bien via WebSocket
|
||||
// (nécessite un client WebSocket de test)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Métriques Kafka à Surveiller
|
||||
|
||||
1. **Lag Consumer** : Délai entre production et consommation
|
||||
2. **Throughput** : Messages/seconde
|
||||
3. **Error Rate** : Taux d'erreur
|
||||
4. **Connection Count** : Nombre de connexions WebSocket actives
|
||||
|
||||
### Endpoint de Santé
|
||||
|
||||
```java
|
||||
@Path("/health/realtime")
|
||||
public class RealtimeHealthResource {
|
||||
|
||||
@GET
|
||||
public Response health() {
|
||||
return Response.ok(Map.of(
|
||||
"websocket_connections", NotificationWebSocketNext.getConnectionCount(),
|
||||
"kafka_consumers", getKafkaConsumerCount(),
|
||||
"status", "healthy"
|
||||
)).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Déploiement
|
||||
|
||||
### Docker Compose (Kafka Local)
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
zookeeper:
|
||||
image: confluentinc/cp-zookeeper:latest
|
||||
environment:
|
||||
ZOOKEEPER_CLIENT_PORT: 2181
|
||||
ZOOKEEPER_TICK_TIME: 2000
|
||||
|
||||
kafka:
|
||||
image: confluentinc/cp-kafka:latest
|
||||
depends_on:
|
||||
- zookeeper
|
||||
ports:
|
||||
- "9092:9092"
|
||||
environment:
|
||||
KAFKA_BROKER_ID: 1
|
||||
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||
```
|
||||
|
||||
### Production (Kubernetes)
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: afterwork-backend
|
||||
spec:
|
||||
replicas: 3 # ✅ Scalabilité horizontale
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: quarkus
|
||||
env:
|
||||
- name: KAFKA_BOOTSTRAP_SERVERS
|
||||
value: "kafka-service:9092"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist d'Implémentation
|
||||
|
||||
### Phase 1 : Setup
|
||||
- [ ] Ajouter dépendances dans `pom.xml`
|
||||
- [ ] Configurer `application.properties`
|
||||
- [ ] Tester Kafka avec Quarkus Dev Services
|
||||
- [ ] Créer les DTOs d'événements
|
||||
|
||||
### Phase 2 : Migration WebSocket
|
||||
- [ ] Créer `NotificationWebSocketNext`
|
||||
- [ ] Créer `ChatWebSocketNext`
|
||||
- [ ] Tester avec le frontend existant
|
||||
- [ ] Comparer performances (avant/après)
|
||||
|
||||
### Phase 3 : Intégration Kafka
|
||||
- [ ] Créer `NotificationKafkaBridge`
|
||||
- [ ] Créer `ChatKafkaBridge`
|
||||
- [ ] Modifier `FriendshipService` pour publier dans Kafka
|
||||
- [ ] Modifier `MessageService` pour publier dans Kafka
|
||||
- [ ] Modifier `SocialPostService` pour les réactions
|
||||
|
||||
### Phase 4 : Frontend
|
||||
- [ ] Améliorer `RealtimeNotificationService` avec heartbeat
|
||||
- [ ] Améliorer `ChatWebSocketService` avec reconnect
|
||||
- [ ] Tester la reconnexion automatique
|
||||
- [ ] Tester multi-device
|
||||
|
||||
### Phase 5 : Tests & Monitoring
|
||||
- [ ] Tests unitaires des bridges
|
||||
- [ ] Tests d'intégration end-to-end
|
||||
- [ ] Configurer monitoring Kafka
|
||||
- [ ] Configurer alertes
|
||||
|
||||
---
|
||||
|
||||
## 📚 Ressources Complémentaires
|
||||
|
||||
- [Quarkus WebSockets Next Tutorial](https://quarkus.io/guides/websockets-next-tutorial)
|
||||
- [Quarkus Kafka Guide](https://quarkus.io/guides/kafka)
|
||||
- [Reactive Messaging HTTP Extension](https://docs.quarkiverse.io/quarkus-reactive-messaging-http/dev/reactive-messaging-websocket.html)
|
||||
- [Kafka Best Practices](https://kafka.apache.org/documentation/#bestPractices)
|
||||
@@ -8860,7 +8860,7 @@ Removed unused interceptor INTERCEPTOR bean [bindings=[@MethodValidated], target
|
||||
{"timestamp":"2026-01-05T22:35:44.20583Z","sequence":8419,"loggerClassName":"org.jboss.logging.DelegatingBasicLogger","loggerName":"org.hibernate.event.internal.AbstractFlushingEventListener","level":"DEBUG","message":"Flushed: 1 insertions, 0 updates, 0 deletions to 1 objects","threadName":"executor-thread-1","threadId":244,"mdc":{},"ndc":"","hostName":"gbanedahoud","processName":"C:\\Program Files\\Java\\jdk-17\\bin\\java.exe","processId":28280}
|
||||
{"timestamp":"2026-01-05T22:35:44.20583Z","sequence":8421,"loggerClassName":"org.jboss.logging.DelegatingBasicLogger","loggerName":"org.hibernate.event.internal.AbstractFlushingEventListener","level":"DEBUG","message":"Flushed: 1 (re)creations, 0 updates, 0 removals to 1 collections","threadName":"executor-thread-1","threadId":244,"mdc":{},"ndc":"","hostName":"gbanedahoud","processName":"C:\\Program Files\\Java\\jdk-17\\bin\\java.exe","processId":28280}
|
||||
{"timestamp":"2026-01-05T22:35:44.20828Z","sequence":8423,"loggerClassName":"org.jboss.logging.DelegatingBasicLogger","loggerName":"org.hibernate.internal.util.EntityPrinter","level":"DEBUG","message":"Listing entities:","threadName":"executor-thread-1","threadId":244,"mdc":{},"ndc":"","hostName":"gbanedahoud","processName":"C:\\Program Files\\Java\\jdk-17\\bin\\java.exe","processId":28280}
|
||||
{"timestamp":"2026-01-05T22:35:44.209296Z","sequence":8424,"loggerClassName":"org.jboss.logging.DelegatingBasicLogger","loggerName":"org.hibernate.internal.util.EntityPrinter","level":"DEBUG","message":"com.lions.dev.entity.users.Users{createdAt=2026-01-05T22:35:44.135208300, motDePasse=$2a$12$bOn5irq0ntL5gZ0MgW3LdeeSpQv6fqKioxRcH/EUiYpw8oVXch9g2, preferredCategory=null, role=USER, favoriteEvents=[], id=9c46a967-dd49-494c-a9b1-cf5bb601f1d0, nom=Dady, profileImageUrl=https://via.placeholder.com/150, email=admin@afterwork.lions.dev, prenoms=One, updatedAt=2026-01-05T22:35:44.135208300}","threadName":"executor-thread-1","threadId":244,"mdc":{},"ndc":"","hostName":"gbanedahoud","processName":"C:\\Program Files\\Java\\jdk-17\\bin\\java.exe","processId":28280}
|
||||
{"timestamp":"2026-01-05T22:35:44.209296Z","sequence":8424,"loggerClassName":"org.jboss.logging.DelegatingBasicLogger","loggerName":"org.hibernate.internal.util.EntityPrinter","level":"DEBUG","message":"com.lions.dev.entity.users.Users{createdAt=2026-01-05T22:35:44.135208300, motDePasse=$2a$12$bOn5irq0ntL5gZ0MgW3LdeeSpQv6fqKioxRcH/EUiYpw8oVXch9g2, preferredCategory=null, role=USER, favoriteEvents=[], id=9c46a967-dd49-494c-a9b1-cf5bb601f1d0, nom=Dady, profileImageUrl=https://placehold.co/150x150.png, email=admin@afterwork.lions.dev, prenoms=One, updatedAt=2026-01-05T22:35:44.135208300}","threadName":"executor-thread-1","threadId":244,"mdc":{},"ndc":"","hostName":"gbanedahoud","processName":"C:\\Program Files\\Java\\jdk-17\\bin\\java.exe","processId":28280}
|
||||
{"timestamp":"2026-01-05T22:35:44.2325806Z","sequence":8425,"loggerClassName":"org.jboss.logging.Logger","loggerName":"org.hibernate.SQL","level":"DEBUG","message":"\r\n \u001b[34minsert\u001b[0m \r\n \u001b[34minto\u001b[0m\r\n users\r\n (created_at, email, mot_de_passe, nom, preferred_category, prenoms, profile_image_url, role, updated_at, id) \r\n \u001b[34mvalues\u001b[0m\r\n (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)","threadName":"executor-thread-1","threadId":244,"mdc":{},"ndc":"","hostName":"gbanedahoud","processName":"C:\\Program Files\\Java\\jdk-17\\bin\\java.exe","processId":28280}
|
||||
[Hibernate]
|
||||
insert
|
||||
|
||||
@@ -29,7 +29,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: afterwork-api
|
||||
image: registry.lions.dev/afterwork-api:1.0.0
|
||||
image: registry.lions.dev/lionsdev/mic-after-work-server-impl-quarkus-main:d659416
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -75,5 +75,5 @@ spec:
|
||||
emptyDir:
|
||||
sizeLimit: 1Gi
|
||||
imagePullSecrets:
|
||||
- name: registry-credentials
|
||||
- name: lionsregistry-secret
|
||||
restartPolicy: Always
|
||||
|
||||
@@ -27,9 +27,7 @@ metadata:
|
||||
|
||||
# WebSocket support
|
||||
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
nginx.ingress.kubernetes.io/websocket-services: "mic-after-work-server-impl-quarkus-main-service"
|
||||
|
||||
# Security headers and CORS
|
||||
nginx.ingress.kubernetes.io/enable-cors: "true"
|
||||
|
||||
13
pom.xml
13
pom.xml
@@ -76,9 +76,20 @@
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
</dependency>
|
||||
<!-- WebSockets Next (remplace quarkus-websockets) -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-websockets</artifactId>
|
||||
<artifactId>quarkus-websockets-next</artifactId>
|
||||
</dependency>
|
||||
<!-- Kafka Reactive Messaging -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-messaging-kafka</artifactId>
|
||||
</dependency>
|
||||
<!-- JSON Serialization pour Kafka -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jsonb</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
75
src/main/java/com/lions/dev/dto/events/ChatMessageEvent.java
Normal file
75
src/main/java/com/lions/dev/dto/events/ChatMessageEvent.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
48
src/main/java/com/lions/dev/dto/events/PresenceEvent.java
Normal file
48
src/main/java/com/lions/dev/dto/events/PresenceEvent.java
Normal 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();
|
||||
}
|
||||
}
|
||||
63
src/main/java/com/lions/dev/dto/events/ReactionEvent.java
Normal file
63
src/main/java/com/lions/dev/dto/events/ReactionEvent.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.lions.dev.dto.request.establishment;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
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
|
||||
@Setter
|
||||
public class EstablishmentCreateRequestDTO {
|
||||
|
||||
@NotNull(message = "Le nom de l'établissement est obligatoire.")
|
||||
@Size(min = 2, max = 200, message = "Le nom doit comporter entre 2 et 200 caractères.")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "Le type d'établissement est obligatoire.")
|
||||
private String type;
|
||||
|
||||
@NotNull(message = "L'adresse est obligatoire.")
|
||||
private String address;
|
||||
|
||||
@NotNull(message = "La ville est obligatoire.")
|
||||
private String city;
|
||||
|
||||
@NotNull(message = "Le code postal est obligatoire.")
|
||||
private String postalCode;
|
||||
|
||||
private String description;
|
||||
private String phoneNumber;
|
||||
private String website;
|
||||
private String priceRange;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.lions.dev.dto.request.establishment;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* DTO pour soumettre ou modifier une note d'établissement.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class EstablishmentRatingRequestDTO {
|
||||
|
||||
@NotNull(message = "La note est obligatoire.")
|
||||
@Min(value = 1, message = "La note doit être au moins 1 étoile.")
|
||||
@Max(value = 5, message = "La note ne peut pas dépasser 5 étoiles.")
|
||||
private Integer rating; // Note de 1 à 5
|
||||
|
||||
@Size(max = 2000, message = "Le commentaire ne peut pas dépasser 2000 caractères.")
|
||||
private String comment; // Commentaire optionnel
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.lions.dev.dto.request.establishment;
|
||||
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* DTO pour la mise à jour d'un établissement.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class EstablishmentUpdateRequestDTO {
|
||||
|
||||
@Size(min = 2, max = 200, message = "Le nom doit comporter entre 2 et 200 caractères.")
|
||||
private String name;
|
||||
|
||||
private String type;
|
||||
private String address;
|
||||
private String city;
|
||||
private String postalCode;
|
||||
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 Double latitude;
|
||||
private Double longitude;
|
||||
}
|
||||
|
||||
@@ -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,15 +32,45 @@ 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
|
||||
|
||||
private Integer maxParticipants; // Nombre maximum de participants autorisés
|
||||
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
|
||||
private String accessibilityInfo; // Informations sur l'accessibilité
|
||||
private String parkingInfo; // Informations sur le parking
|
||||
private String securityProtocol; // Protocole de sécurité de l'événement
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.lions.dev.dto.response.establishment;
|
||||
|
||||
import com.lions.dev.entity.establishment.EstablishmentMedia;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour renvoyer les informations d'un média d'établissement.
|
||||
*/
|
||||
@Getter
|
||||
public class EstablishmentMediaResponseDTO {
|
||||
|
||||
private String id;
|
||||
private String establishmentId;
|
||||
private String mediaUrl;
|
||||
private String mediaType; // "PHOTO" ou "VIDEO"
|
||||
private String thumbnailUrl;
|
||||
private MediaUploaderDTO uploadedBy;
|
||||
private LocalDateTime uploadedAt;
|
||||
private Integer displayOrder;
|
||||
|
||||
/**
|
||||
* Constructeur qui transforme une entité EstablishmentMedia en DTO.
|
||||
*
|
||||
* @param media Le média à convertir en DTO.
|
||||
*/
|
||||
public EstablishmentMediaResponseDTO(EstablishmentMedia media) {
|
||||
this.id = media.getId().toString();
|
||||
this.establishmentId = media.getEstablishment().getId().toString();
|
||||
this.mediaUrl = media.getMediaUrl();
|
||||
this.mediaType = media.getMediaType().name();
|
||||
this.thumbnailUrl = media.getThumbnailUrl();
|
||||
this.uploadedAt = media.getUploadedAt();
|
||||
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().getFirstName(),
|
||||
media.getUploadedBy().getLastName(),
|
||||
media.getUploadedBy().getProfileImageUrl()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO interne pour les informations de l'uploader.
|
||||
*/
|
||||
@Getter
|
||||
public static class MediaUploaderDTO {
|
||||
private final String id;
|
||||
private final String firstName;
|
||||
private final String lastName;
|
||||
private final String profileImageUrl;
|
||||
|
||||
public MediaUploaderDTO(String id, String firstName, String lastName, String profileImageUrl) {
|
||||
this.id = id;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.profileImageUrl = profileImageUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.lions.dev.dto.response.establishment;
|
||||
|
||||
import com.lions.dev.entity.establishment.EstablishmentRating;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO pour renvoyer les informations d'une note d'établissement.
|
||||
*/
|
||||
@Getter
|
||||
public class EstablishmentRatingResponseDTO {
|
||||
|
||||
private String id;
|
||||
private String establishmentId;
|
||||
private String userId;
|
||||
private Integer rating;
|
||||
private String comment;
|
||||
private LocalDateTime ratedAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* Constructeur qui transforme une entité EstablishmentRating en DTO.
|
||||
*
|
||||
* @param rating La note à convertir en DTO.
|
||||
*/
|
||||
public EstablishmentRatingResponseDTO(EstablishmentRating rating) {
|
||||
this.id = rating.getId().toString();
|
||||
this.establishmentId = rating.getEstablishment().getId().toString();
|
||||
this.userId = rating.getUser().getId().toString();
|
||||
this.rating = rating.getRating();
|
||||
this.comment = rating.getComment();
|
||||
this.ratedAt = rating.getRatedAt();
|
||||
this.updatedAt = rating.getUpdatedAt();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.lions.dev.dto.response.establishment;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* DTO pour renvoyer les statistiques de notation d'un établissement.
|
||||
*/
|
||||
@Getter
|
||||
public class EstablishmentRatingStatsResponseDTO {
|
||||
|
||||
private Double averageRating; // Note moyenne (0.0 à 5.0)
|
||||
private Integer totalRatings; // Nombre total de notes
|
||||
private Map<Integer, Integer> distribution; // Distribution par étoile {5: 10, 4: 5, ...}
|
||||
|
||||
/**
|
||||
* Constructeur pour créer les statistiques de notation.
|
||||
*
|
||||
* @param averageRating La note moyenne
|
||||
* @param totalRatings Le nombre total de notes
|
||||
* @param distribution La distribution des notes par étoile
|
||||
*/
|
||||
public EstablishmentRatingStatsResponseDTO(Double averageRating, Integer totalRatings, Map<Integer, Integer> distribution) {
|
||||
this.averageRating = averageRating;
|
||||
this.totalRatings = totalRatings;
|
||||
this.distribution = distribution;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
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).
|
||||
*/
|
||||
@Getter
|
||||
public class EstablishmentResponseDTO {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String type;
|
||||
private String address;
|
||||
private String city;
|
||||
private String postalCode;
|
||||
private String description;
|
||||
private String phoneNumber;
|
||||
private String website;
|
||||
private Double averageRating; // Note moyenne calculée
|
||||
private Integer totalReviewsCount; // v2.0 - renommé depuis totalRatingsCount
|
||||
private String priceRange;
|
||||
private String verificationStatus; // v2.0 - PENDING, VERIFIED, REJECTED
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private String managerId;
|
||||
private String managerEmail;
|
||||
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é
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
public EstablishmentResponseDTO(Establishment establishment) {
|
||||
this.id = establishment.getId().toString();
|
||||
this.name = establishment.getName();
|
||||
this.type = establishment.getType();
|
||||
this.address = establishment.getAddress();
|
||||
this.city = establishment.getCity();
|
||||
this.postalCode = establishment.getPostalCode();
|
||||
this.description = establishment.getDescription();
|
||||
this.phoneNumber = establishment.getPhoneNumber();
|
||||
this.website = establishment.getWebsite();
|
||||
this.averageRating = establishment.getAverageRating();
|
||||
this.totalReviewsCount = establishment.getTotalReviewsCount(); // v2.0
|
||||
this.priceRange = establishment.getPriceRange();
|
||||
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().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é
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.lions.dev.dto.response.events;
|
||||
|
||||
import com.lions.dev.entity.events.Events;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour renvoyer les informations 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é pour structurer les données retournées dans les réponses
|
||||
* après les opérations sur les événements (création, récupération).
|
||||
*/
|
||||
@@ -16,33 +22,89 @@ public class EventCreateResponseDTO {
|
||||
private String description; // Description de l'événement
|
||||
private LocalDateTime startDate; // Date de début de l'événement
|
||||
private LocalDateTime endDate; // Date de fin de l'événement
|
||||
private String location; // Lieu de l'événement
|
||||
private String establishmentId; // v2.0 - ID de l'établissement où se déroule l'événement
|
||||
private String establishmentName; // v2.0 - Nom de l'établissement
|
||||
private String category; // Catégorie de l'événement
|
||||
private String link; // Lien vers plus d'informations
|
||||
private String imageUrl; // URL d'une image pour l'événement
|
||||
private String creatorId; // ID du créateur de l'événement
|
||||
private String creatorEmail; // Email du créateur de l'événement
|
||||
private String creatorFirstName; // Prénom du créateur de l'événement
|
||||
private String creatorLastName; // Nom de famille du création de l'événement
|
||||
private String status; // Statut de l'événement
|
||||
private String creatorFirstName; // v2.0 - Prénom du créateur de l'événement
|
||||
private String creatorLastName; // v2.0 - Nom de famille du créateur de l'événement
|
||||
private String status; // Statut de l'événement (OPEN, CLOSED, CANCELLED, COMPLETED)
|
||||
private Boolean isPrivate; // v2.0 - Indique si l'événement est privé
|
||||
private Boolean waitlistEnabled; // v2.0 - Indique si la liste d'attente est activée
|
||||
private Integer maxParticipants; // Nombre maximum de participants autorisés
|
||||
private Integer participationFee; // Frais de participation en centimes
|
||||
private Long reactionsCount; // ✅ Nombre de réactions (utilisateurs qui ont cet événement en favori)
|
||||
private Boolean isFavorite; // ✅ Indique si l'utilisateur actuel a cet événement en favori (optionnel, dépend du contexte)
|
||||
|
||||
// Champ déprécié (v1.0) - conservé pour compatibilité
|
||||
/**
|
||||
* @deprecated Utiliser {@link #establishmentId} et {@link #establishmentName} à la place.
|
||||
*/
|
||||
@Deprecated
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* Constructeur qui transforme une entité Events en DTO.
|
||||
* Constructeur qui transforme une entité Events en DTO (v2.0).
|
||||
* Utilise UsersRepository pour calculer reactionsCount et isFavorite.
|
||||
*
|
||||
* @param event L'événement à convertir en DTO.
|
||||
* @param usersRepository Le repository pour compter les réactions (peut être null).
|
||||
* @param currentUserId L'ID de l'utilisateur actuel pour vérifier isFavorite (peut être null).
|
||||
*/
|
||||
public EventCreateResponseDTO(Events event) {
|
||||
public EventCreateResponseDTO(Events event, UsersRepository usersRepository, UUID currentUserId) {
|
||||
this.id = event.getId().toString();
|
||||
this.title = event.getTitle();
|
||||
this.description = event.getDescription();
|
||||
this.startDate = event.getStartDate();
|
||||
this.endDate = event.getEndDate();
|
||||
this.location = event.getLocation();
|
||||
this.category = event.getCategory();
|
||||
this.link = event.getLink();
|
||||
this.imageUrl = event.getImageUrl();
|
||||
this.creatorEmail = event.getCreator().getEmail();
|
||||
this.creatorFirstName = event.getCreator().getPrenoms();
|
||||
this.creatorLastName = event.getCreator().getNom();
|
||||
this.status = event.getStatus();
|
||||
this.isPrivate = event.getIsPrivate(); // v2.0
|
||||
this.waitlistEnabled = event.getWaitlistEnabled(); // v2.0
|
||||
this.maxParticipants = event.getMaxParticipants();
|
||||
this.participationFee = event.getParticipationFee();
|
||||
|
||||
// ✅ Calculer reactionsCount si usersRepository est fourni
|
||||
if (usersRepository != null) {
|
||||
this.reactionsCount = usersRepository.countUsersWithFavoriteEvent(event.getId());
|
||||
} else {
|
||||
this.reactionsCount = 0L;
|
||||
}
|
||||
|
||||
// ✅ Vérifier isFavorite si currentUserId est fourni
|
||||
if (currentUserId != null && usersRepository != null) {
|
||||
this.isFavorite = usersRepository.hasUserFavoriteEvent(currentUserId, event.getId());
|
||||
} else {
|
||||
this.isFavorite = null;
|
||||
}
|
||||
|
||||
// v2.0 - Informations sur l'établissement
|
||||
if (event.getEstablishment() != null) {
|
||||
this.establishmentId = event.getEstablishment().getId().toString();
|
||||
this.establishmentName = event.getEstablishment().getName();
|
||||
this.location = event.getLocation(); // Méthode qui retourne l'adresse de l'établissement
|
||||
}
|
||||
|
||||
// v2.0 - Informations sur le créateur
|
||||
if (event.getCreator() != null) {
|
||||
this.creatorId = event.getCreator().getId().toString();
|
||||
this.creatorEmail = event.getCreator().getEmail();
|
||||
this.creatorFirstName = event.getCreator().getFirstName(); // v2.0
|
||||
this.creatorLastName = event.getCreator().getLastName(); // v2.0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructeur simplifié sans calcul de réactions (pour compatibilité).
|
||||
*
|
||||
* @param event L'événement à convertir en DTO.
|
||||
*/
|
||||
public EventCreateResponseDTO(Events event) {
|
||||
this(event, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; // Roğe 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
105
src/main/java/com/lions/dev/entity/booking/Booking.java
Normal file
105
src/main/java/com/lions/dev/entity/booking/Booking.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ public class Conversation extends BaseEntity {
|
||||
this.lastMessageSender = message.getSender();
|
||||
|
||||
// Incrémenter le compteur de non-lus pour le destinataire
|
||||
if (message.getSender().equals(user1)) {
|
||||
if (message.getSender() != null && user1 != null && message.getSender().getId().equals(user1.getId())) {
|
||||
unreadCountUser2++;
|
||||
} else {
|
||||
} else if (message.getSender() != null && user2 != null && message.getSender().getId().equals(user2.getId())) {
|
||||
unreadCountUser1++;
|
||||
}
|
||||
|
||||
@@ -87,10 +87,10 @@ public class Conversation extends BaseEntity {
|
||||
* Marque tous les messages comme lus pour un utilisateur.
|
||||
*/
|
||||
public void markAllAsReadForUser(Users user) {
|
||||
if (user.equals(user1)) {
|
||||
if (user != null && user1 != null && user.getId().equals(user1.getId())) {
|
||||
unreadCountUser1 = 0;
|
||||
System.out.println("[LOG] Messages marqués comme lus pour user1 dans la conversation " + this.getId());
|
||||
} else if (user.equals(user2)) {
|
||||
} else if (user != null && user2 != null && user.getId().equals(user2.getId())) {
|
||||
unreadCountUser2 = 0;
|
||||
System.out.println("[LOG] Messages marqués comme lus pour user2 dans la conversation " + this.getId());
|
||||
}
|
||||
@@ -100,9 +100,9 @@ public class Conversation extends BaseEntity {
|
||||
* Récupère le nombre de messages non lus pour un utilisateur.
|
||||
*/
|
||||
public int getUnreadCountForUser(Users user) {
|
||||
if (user.equals(user1)) {
|
||||
if (user != null && user1 != null && user.getId().equals(user1.getId())) {
|
||||
return unreadCountUser1;
|
||||
} else if (user.equals(user2)) {
|
||||
} else if (user != null && user2 != null && user.getId().equals(user2.getId())) {
|
||||
return unreadCountUser2;
|
||||
}
|
||||
return 0;
|
||||
@@ -112,9 +112,9 @@ public class Conversation extends BaseEntity {
|
||||
* Récupère l'autre utilisateur de la conversation.
|
||||
*/
|
||||
public Users getOtherUser(Users user) {
|
||||
if (user.equals(user1)) {
|
||||
if (user != null && user1 != null && user.getId().equals(user1.getId())) {
|
||||
return user2;
|
||||
} else if (user.equals(user2)) {
|
||||
} else if (user != null && user2 != null && user.getId().equals(user2.getId())) {
|
||||
return user1;
|
||||
}
|
||||
return null;
|
||||
@@ -124,6 +124,8 @@ public class Conversation extends BaseEntity {
|
||||
* Vérifie si un utilisateur fait partie de cette conversation.
|
||||
*/
|
||||
public boolean containsUser(Users user) {
|
||||
return user.equals(user1) || user.equals(user2);
|
||||
if (user == null) return false;
|
||||
return (user1 != null && user.getId().equals(user1.getId())) ||
|
||||
(user2 != null && user.getId().equals(user2.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "establishments")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class Establishment extends BaseEntity {
|
||||
|
||||
@Column(name = "name", nullable = false)
|
||||
private String name; // Le nom de l'établissement
|
||||
|
||||
@Column(name = "type", nullable = false)
|
||||
private String type; // Type d'établissement (bar, restaurant, club, etc.)
|
||||
|
||||
@Column(name = "address", nullable = false)
|
||||
private String address; // Adresse de l'établissement
|
||||
|
||||
@Column(name = "city", nullable = false)
|
||||
private String city; // Ville de l'établissement
|
||||
|
||||
@Column(name = "postal_code", nullable = false)
|
||||
private String postalCode; // Code postal
|
||||
|
||||
@Column(name = "description", length = 2000)
|
||||
private String description; // Description de l'établissement
|
||||
|
||||
@Column(name = "phone_number")
|
||||
private String phoneNumber; // Numéro de téléphone
|
||||
|
||||
@Column(name = "website")
|
||||
private String website; // Site web
|
||||
|
||||
@Column(name = "average_rating")
|
||||
private Double averageRating; // Note moyenne calculée (0.0 à 5.0)
|
||||
|
||||
@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 (LOW, MEDIUM, HIGH, PREMIUM)
|
||||
|
||||
@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
|
||||
|
||||
@Column(name = "longitude")
|
||||
private Double longitude; // Longitude pour la géolocalisation
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "manager_id", nullable = false)
|
||||
private Users manager; // Le responsable de l'établissement
|
||||
|
||||
// Relations avec les médias et les notes
|
||||
@OneToMany(mappedBy = "establishment", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
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 (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.
|
||||
*/
|
||||
public Establishment(String name, String type, String address, String city,
|
||||
String postalCode, Users manager) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.address = address;
|
||||
this.city = city;
|
||||
this.postalCode = postalCode;
|
||||
this.manager = manager;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Entité représentant un média (photo ou vidéo) associé à un établissement.
|
||||
*
|
||||
* Un établissement peut avoir plusieurs médias pour promouvoir son établissement.
|
||||
* Les médias sont uploadés par le responsable de l'établissement.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "establishment_media")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class EstablishmentMedia extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "establishment_id", nullable = false)
|
||||
private Establishment establishment; // L'établissement auquel ce média appartient
|
||||
|
||||
@Column(name = "media_url", nullable = false, length = 1000)
|
||||
private String mediaUrl; // URL du média (photo ou vidéo)
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "media_type", nullable = false)
|
||||
private MediaType mediaType; // Type de média (PHOTO ou VIDEO)
|
||||
|
||||
@Column(name = "thumbnail_url", length = 1000)
|
||||
private String thumbnailUrl; // URL de la miniature (pour les vidéos)
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "uploaded_by", nullable = false)
|
||||
private Users uploadedBy; // L'utilisateur qui a uploadé le média
|
||||
|
||||
@Column(name = "uploaded_at", nullable = false)
|
||||
private java.time.LocalDateTime uploadedAt; // Date et heure d'upload
|
||||
|
||||
@Column(name = "display_order", nullable = false)
|
||||
private Integer displayOrder = 0; // Ordre d'affichage dans la galerie
|
||||
|
||||
/**
|
||||
* Constructeur pour créer un média avec les informations de base.
|
||||
*/
|
||||
public EstablishmentMedia(Establishment establishment, String mediaUrl, MediaType mediaType, Users uploadedBy) {
|
||||
this.establishment = establishment;
|
||||
this.mediaUrl = mediaUrl;
|
||||
this.mediaType = mediaType;
|
||||
this.uploadedBy = uploadedBy;
|
||||
this.uploadedAt = java.time.LocalDateTime.now();
|
||||
this.displayOrder = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Entité représentant une note donnée par un utilisateur à un établissement.
|
||||
*
|
||||
* Chaque utilisateur ne peut avoir qu'une seule note par établissement.
|
||||
* La note est un entier entre 1 et 5 (étoiles).
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
name = "establishment_ratings",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(
|
||||
name = "uk_establishment_user_rating",
|
||||
columnNames = {"establishment_id", "user_id"}
|
||||
)
|
||||
}
|
||||
)
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class EstablishmentRating extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "establishment_id", nullable = false)
|
||||
private Establishment establishment; // L'établissement noté
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private Users user; // L'utilisateur qui a donné la note
|
||||
|
||||
@Column(name = "rating", nullable = false)
|
||||
private Integer rating; // Note donnée (1 à 5 étoiles)
|
||||
|
||||
@Column(name = "comment", length = 2000)
|
||||
private String comment; // Commentaire optionnel accompagnant la note
|
||||
|
||||
@Column(name = "rated_at", nullable = false, updatable = false)
|
||||
private java.time.LocalDateTime ratedAt; // Date et heure de création de la note
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private java.time.LocalDateTime updatedAt; // Date et heure de dernière modification
|
||||
|
||||
/**
|
||||
* Constructeur pour créer une note avec les informations de base.
|
||||
*/
|
||||
public EstablishmentRating(Establishment establishment, Users user, Integer rating) {
|
||||
this.establishment = establishment;
|
||||
this.user = user;
|
||||
this.rating = rating;
|
||||
this.ratedAt = java.time.LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la note et la date de modification.
|
||||
*/
|
||||
public void updateRating(Integer newRating, String newComment) {
|
||||
this.rating = newRating;
|
||||
this.comment = newComment;
|
||||
this.updatedAt = java.time.LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.lions.dev.entity.establishment;
|
||||
|
||||
/**
|
||||
* Enum représentant le type de média pour un établissement.
|
||||
*/
|
||||
public enum MediaType {
|
||||
PHOTO,
|
||||
VIDEO
|
||||
}
|
||||
|
||||
92
src/main/java/com/lions/dev/entity/establishment/Review.java
Normal file
92
src/main/java/com/lions/dev/entity/establishment/Review.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,43 @@ 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
|
||||
|
||||
@Column(name = "tags", length = 500)
|
||||
private String tags; // Tags/mots-clés associés à l'événement (séparés par des virgules)
|
||||
|
||||
@Column(name = "organizer", length = 200)
|
||||
private String organizer; // Nom de l'organisateur de l'événement
|
||||
|
||||
@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
|
||||
|
||||
@Column(name = "transport_info", length = 1000)
|
||||
private String transportInfo; // Informations sur les transports disponibles
|
||||
|
||||
@Column(name = "accommodation_info", length = 1000)
|
||||
private String accommodationInfo; // Informations sur l'hébergement
|
||||
|
||||
@Column(name = "accessibility_info", length = 1000)
|
||||
private String accessibilityInfo; // Informations sur l'accessibilité
|
||||
|
||||
@Column(name = "parking_info", length = 1000)
|
||||
private String parkingInfo; // Informations sur le parking
|
||||
|
||||
@Column(name = "security_protocol", length = 1000)
|
||||
private String securityProtocol; // Protocole de sécurité de l'événement
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "creator_id", nullable = false)
|
||||
@@ -101,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
|
||||
|
||||
|
||||
119
src/main/java/com/lions/dev/entity/promotion/Promotion.java
Normal file
119
src/main/java/com/lions/dev/entity/promotion/Promotion.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.lions.dev.entity.users;
|
||||
import com.lions.dev.entity.BaseEntity;
|
||||
import com.lions.dev.entity.events.Events;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import lombok.Getter;
|
||||
@@ -13,6 +15,10 @@ import at.favre.lib.crypto.bcrypt.BCrypt;
|
||||
|
||||
/**
|
||||
* Représentation de l'entité Utilisateur 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).
|
||||
*
|
||||
* Cette entité contient les informations de base sur un utilisateur, telles que le nom,
|
||||
* les prénoms, l'email, le mot de passe haché, et son rôle.
|
||||
*
|
||||
@@ -26,17 +32,17 @@ import at.favre.lib.crypto.bcrypt.BCrypt;
|
||||
@ToString
|
||||
public class Users extends BaseEntity {
|
||||
|
||||
@Column(name = "nom", nullable = false, length = 100)
|
||||
private String nom; // Le nom de l'utilisateur
|
||||
@Column(name = "first_name", nullable = false, length = 100)
|
||||
private String firstName; // Le prénom de l'utilisateur (v2.0)
|
||||
|
||||
@Column(name = "prenoms", nullable = false, length = 100)
|
||||
private String prenoms; // Les prénoms de l'utilisateur
|
||||
@Column(name = "last_name", nullable = false, length = 100)
|
||||
private String lastName; // Le nom de famille de l'utilisateur (v2.0)
|
||||
|
||||
@Column(name = "email", nullable = false, unique = true, length = 100)
|
||||
private String email; // L'adresse email unique de l'utilisateur
|
||||
|
||||
@Column(name = "mot_de_passe", nullable = false)
|
||||
private String motDePasse; // Mot de passe haché avec BCrypt
|
||||
@Column(name = "password_hash", nullable = false)
|
||||
private String passwordHash; // Mot de passe haché avec BCrypt (v2.0)
|
||||
|
||||
@Column(name = "role", nullable = false)
|
||||
private String role; // Le rôle de l'utilisateur (ADMIN, MODERATOR, USER, etc.)
|
||||
@@ -44,8 +50,15 @@ public class Users extends BaseEntity {
|
||||
@Column(name = "profile_image_url")
|
||||
private String profileImageUrl; // L'URL de l'image de profil de l'utilisateur
|
||||
|
||||
@Column(name = "preferred_category")
|
||||
private String preferredCategory; // La catégorie préférée de l'utilisateur
|
||||
@Column(name = "bio", length = 500)
|
||||
private String bio; // Biographie courte de l'utilisateur (v2.0)
|
||||
|
||||
@Column(name = "loyalty_points", nullable = false)
|
||||
private Integer loyaltyPoints = 0; // Points de fidélité accumulés (v2.0)
|
||||
|
||||
@Column(name = "preferences", nullable = false)
|
||||
@org.hibernate.annotations.JdbcTypeCode(org.hibernate.type.SqlTypes.JSON)
|
||||
private java.util.Map<String, Object> preferences = new java.util.HashMap<>(); // Préférences utilisateur en JSON (v2.0)
|
||||
|
||||
@Column(name = "is_verified", nullable = false)
|
||||
private boolean isVerified = false; // Indique si l'utilisateur est vérifié (compte officiel)
|
||||
@@ -60,28 +73,57 @@ public class Users extends BaseEntity {
|
||||
// private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
|
||||
/**
|
||||
* Hache le mot de passe avec BCrypt et le stocke dans l'attribut `motDePasse`.
|
||||
* Hache le mot de passe avec BCrypt et le stocke dans l'attribut `passwordHash`.
|
||||
* Version 2.0 - Utilise passwordHash au lieu de motDePasse.
|
||||
*
|
||||
* @param motDePasse Le mot de passe en texte clair à hacher.
|
||||
* @param password Le mot de passe en texte clair à hacher.
|
||||
*/
|
||||
public void setMotDePasse(String motDePasse) {
|
||||
this.motDePasse = BCrypt.withDefaults().hashToString(12, motDePasse.toCharArray());
|
||||
public void setPassword(String password) {
|
||||
this.passwordHash = BCrypt.withDefaults().hashToString(12, password.toCharArray());
|
||||
System.out.println("[LOG] Mot de passe haché pour l'utilisateur : " + this.email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que le mot de passe fourni correspond au mot de passe haché de l'utilisateur.
|
||||
* Définit directement le hash du mot de passe (pour compatibilité).
|
||||
*
|
||||
* @param motDePasse Le mot de passe en texte clair à vérifier.
|
||||
* @param passwordHash Le hash du mot de passe.
|
||||
*/
|
||||
public void setPasswordHash(String passwordHash) {
|
||||
this.passwordHash = passwordHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que le mot de passe fourni correspond au mot de passe haché de l'utilisateur.
|
||||
* Version 2.0 - Utilise passwordHash au lieu de motDePasse.
|
||||
*
|
||||
* @param password Le mot de passe en texte clair à vérifier.
|
||||
* @return true si le mot de passe est correct, false sinon.
|
||||
*/
|
||||
public boolean verifierMotDePasse(String motDePasse) {
|
||||
BCrypt.Result result = BCrypt.verifyer().verify(motDePasse.toCharArray(), this.motDePasse);
|
||||
public boolean verifyPassword(String password) {
|
||||
BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), this.passwordHash);
|
||||
boolean isValid = result.verified;
|
||||
System.out.println("[LOG] Vérification du mot de passe pour l'utilisateur : " + this.email + " - Résultat : " + isValid);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de compatibilité avec l'ancienne API (dépréciée).
|
||||
* @deprecated Utiliser {@link #setPassword(String)} à la place.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setMotDePasse(String motDePasse) {
|
||||
setPassword(motDePasse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode de compatibilité avec l'ancienne API (dépréciée).
|
||||
* @deprecated Utiliser {@link #verifyPassword(String)} à la place.
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean verifierMotDePasse(String motDePasse) {
|
||||
return verifyPassword(motDePasse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur a le rôle d'administrateur.
|
||||
*
|
||||
@@ -93,8 +135,12 @@ public class Users extends BaseEntity {
|
||||
return isAdmin;
|
||||
}
|
||||
|
||||
@OneToMany(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "favorite_events")
|
||||
@ManyToMany(fetch = FetchType.LAZY)
|
||||
@JoinTable(
|
||||
name = "user_favorite_events",
|
||||
joinColumns = @JoinColumn(name = "user_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "event_id")
|
||||
)
|
||||
private Set<Events> favoriteEvents = new HashSet<>(); // Liste des événements favoris
|
||||
|
||||
/**
|
||||
@@ -107,6 +153,26 @@ public class Users extends BaseEntity {
|
||||
System.out.println("[LOG] Événement ajouté aux favoris pour l'utilisateur : " + this.email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retire un événement des favoris de l'utilisateur.
|
||||
*
|
||||
* @param event L'événement à retirer.
|
||||
*/
|
||||
public void removeFavoriteEvent(Events event) {
|
||||
favoriteEvents.remove(event);
|
||||
System.out.println("[LOG] Événement retiré des favoris pour l'utilisateur : " + this.email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un événement est dans les favoris de l'utilisateur.
|
||||
*
|
||||
* @param event L'événement à vérifier.
|
||||
* @return true si l'événement est favori, false sinon.
|
||||
*/
|
||||
public boolean hasFavoriteEvent(Events event) {
|
||||
return favoriteEvents.contains(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la liste des événements favoris de l'utilisateur.
|
||||
*
|
||||
@@ -118,25 +184,44 @@ public class Users extends BaseEntity {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la catégorie préférée de l'utilisateur.
|
||||
* Retourne la catégorie préférée de l'utilisateur depuis preferences (v2.0).
|
||||
*
|
||||
* @return La catégorie préférée de l'utilisateur.
|
||||
* @return La catégorie préférée de l'utilisateur, ou null si non définie.
|
||||
*/
|
||||
public String getPreferredCategory() {
|
||||
System.out.println("[LOG] Récupération de la catégorie préférée pour l'utilisateur : " + this.email);
|
||||
return preferredCategory;
|
||||
if (preferences != null && preferences.containsKey("preferred_category")) {
|
||||
Object category = preferences.get("preferred_category");
|
||||
return category != null ? category.toString() : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la catégorie préférée de l'utilisateur.
|
||||
* Définit la catégorie préférée de l'utilisateur dans preferences (v2.0).
|
||||
*
|
||||
* @param category La catégorie à définir.
|
||||
*/
|
||||
public void setPreferredCategory(String category) {
|
||||
this.preferredCategory = category;
|
||||
if (preferences == null) {
|
||||
preferences = new java.util.HashMap<>();
|
||||
}
|
||||
if (category != null) {
|
||||
preferences.put("preferred_category", category);
|
||||
} else {
|
||||
preferences.remove("preferred_category");
|
||||
}
|
||||
System.out.println("[LOG] Catégorie préférée définie pour l'utilisateur : " + this.email + " - Catégorie : " + category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la présence de l'utilisateur (marque comme en ligne et met à jour lastSeen).
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.lions.dev.repository;
|
||||
|
||||
import com.lions.dev.entity.establishment.EstablishmentMedia;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository Panache pour les médias d'établissements.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class EstablishmentMediaRepository implements PanacheRepositoryBase<EstablishmentMedia, UUID> {
|
||||
|
||||
/**
|
||||
* Récupère tous les médias d'un établissement, triés par ordre d'affichage.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @return Liste des médias de l'établissement
|
||||
*/
|
||||
public List<EstablishmentMedia> findByEstablishmentId(UUID establishmentId) {
|
||||
return find("establishment.id = ?1 ORDER BY displayOrder ASC", establishmentId).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime tous les médias d'un établissement.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
*/
|
||||
public void deleteByEstablishmentId(UUID establishmentId) {
|
||||
delete("establishment.id = ?1", establishmentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.lions.dev.repository;
|
||||
|
||||
import com.lions.dev.entity.establishment.EstablishmentRating;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Repository Panache pour les notations d'établissements.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class EstablishmentRatingRepository implements PanacheRepositoryBase<EstablishmentRating, UUID> {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les notes d'un établissement.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @return Liste des notes de l'établissement
|
||||
*/
|
||||
public List<EstablishmentRating> findByEstablishmentId(UUID establishmentId) {
|
||||
return find("establishment.id = ?1 ORDER BY ratedAt DESC", establishmentId).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la note moyenne d'un établissement.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @return La note moyenne (0.0 si aucune note)
|
||||
*/
|
||||
public Double calculateAverageRating(UUID establishmentId) {
|
||||
List<EstablishmentRating> ratings = findByEstablishmentId(establishmentId);
|
||||
if (ratings.isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
return ratings.stream()
|
||||
.mapToInt(EstablishmentRating::getRating)
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la distribution des notes pour un établissement.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @return Map avec clé = nombre d'étoiles (1-5), valeur = nombre de notes
|
||||
*/
|
||||
public Map<Integer, Integer> calculateRatingDistribution(UUID establishmentId) {
|
||||
List<EstablishmentRating> ratings = findByEstablishmentId(establishmentId);
|
||||
return ratings.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
EstablishmentRating::getRating,
|
||||
Collectors.collectingAndThen(Collectors.counting(), Long::intValue)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre total de notes pour un établissement.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @return Le nombre total de notes
|
||||
*/
|
||||
public Long countByEstablishmentId(UUID establishmentId) {
|
||||
return count("establishment.id = ?1", establishmentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.lions.dev.repository;
|
||||
|
||||
import com.lions.dev.entity.establishment.Establishment;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Repository pour l'entité Establishment.
|
||||
* Ce repository gère les opérations de base (CRUD) sur les établissements.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class EstablishmentRepository implements PanacheRepositoryBase<Establishment, UUID> {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentRepository.class);
|
||||
|
||||
/**
|
||||
* Récupère tous les établissements gérés par un responsable.
|
||||
*
|
||||
* @param managerId L'ID du responsable.
|
||||
* @return Une liste d'établissements gérés par ce responsable.
|
||||
*/
|
||||
public List<Establishment> findByManagerId(UUID managerId) {
|
||||
LOG.info("[LOG] Récupération des établissements du responsable : " + managerId);
|
||||
List<Establishment> establishments = list("manager.id = ?1", managerId);
|
||||
LOG.info("[LOG] Nombre d'établissements trouvés pour le responsable " + managerId + " : " + establishments.size());
|
||||
return establishments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche des établissements par nom ou ville.
|
||||
*
|
||||
* @param query Le terme de recherche.
|
||||
* @return Une liste d'établissements correspondant à la recherche.
|
||||
*/
|
||||
public List<Establishment> searchByNameOrCity(String query) {
|
||||
LOG.info("[LOG] Recherche d'établissements avec la requête : " + query);
|
||||
String searchPattern = "%" + query.toLowerCase() + "%";
|
||||
List<Establishment> establishments = list(
|
||||
"LOWER(name) LIKE ?1 OR LOWER(city) LIKE ?1",
|
||||
searchPattern
|
||||
);
|
||||
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
|
||||
return establishments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtre les établissements par type.
|
||||
*
|
||||
* @param type Le type d'établissement.
|
||||
* @return Une liste d'établissements du type spécifié.
|
||||
*/
|
||||
public List<Establishment> findByType(String type) {
|
||||
LOG.info("[LOG] Filtrage des établissements par type : " + type);
|
||||
List<Establishment> establishments = list("type = ?1", type);
|
||||
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
|
||||
return establishments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtre les établissements par ville.
|
||||
*
|
||||
* @param city La ville.
|
||||
* @return Une liste d'établissements dans cette ville.
|
||||
*/
|
||||
public List<Establishment> findByCity(String city) {
|
||||
LOG.info("[LOG] Filtrage des établissements par ville : " + city);
|
||||
List<Establishment> establishments = list("city = ?1", city);
|
||||
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
|
||||
return establishments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtre les établissements par fourchette de prix.
|
||||
*
|
||||
* @param priceRange La fourchette de prix.
|
||||
* @return Une liste d'établissements avec cette fourchette de prix.
|
||||
*/
|
||||
public List<Establishment> findByPriceRange(String priceRange) {
|
||||
LOG.info("[LOG] Filtrage des établissements par fourchette de prix : " + priceRange);
|
||||
List<Establishment> establishments = list("priceRange = ?1", priceRange);
|
||||
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
|
||||
return establishments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.lions.dev.repository;
|
||||
|
||||
import com.lions.dev.entity.events.Events;
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -15,6 +18,43 @@ import java.util.UUID;
|
||||
@ApplicationScoped
|
||||
public class UsersRepository implements PanacheRepositoryBase<Users, UUID> {
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager entityManager;
|
||||
|
||||
/**
|
||||
* Compte le nombre d'utilisateurs qui ont un événement en favori.
|
||||
*
|
||||
* @param eventId L'ID de l'événement
|
||||
* @return Le nombre d'utilisateurs qui ont cet événement en favori
|
||||
*/
|
||||
public long countUsersWithFavoriteEvent(UUID eventId) {
|
||||
// ✅ Utiliser la table de jointure user_favorite_events
|
||||
return entityManager.createQuery(
|
||||
"SELECT COUNT(u) FROM Users u JOIN u.favoriteEvents e WHERE e.id = :eventId",
|
||||
Long.class
|
||||
)
|
||||
.setParameter("eventId", eventId)
|
||||
.getSingleResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un utilisateur a un événement en favori.
|
||||
*
|
||||
* @param userId L'ID de l'utilisateur
|
||||
* @param eventId L'ID de l'événement
|
||||
* @return true si l'utilisateur a cet événement en favori, false sinon
|
||||
*/
|
||||
public boolean hasUserFavoriteEvent(UUID userId, UUID eventId) {
|
||||
Long count = entityManager.createQuery(
|
||||
"SELECT COUNT(u) FROM Users u JOIN u.favoriteEvents e WHERE u.id = :userId AND e.id = :eventId",
|
||||
Long.class
|
||||
)
|
||||
.setParameter("userId", userId)
|
||||
.setParameter("eventId", eventId)
|
||||
.getSingleResult();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche un utilisateur par son adresse email.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
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;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des médias d'établissements.
|
||||
* Cette classe expose des endpoints pour uploader, récupérer et supprimer des médias.
|
||||
*/
|
||||
@Path("/establishments/{establishmentId}/media")
|
||||
@Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
@Consumes(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Establishment Media", description = "Opérations liées aux médias des établissements")
|
||||
public class EstablishmentMediaResource {
|
||||
|
||||
@Inject
|
||||
EstablishmentMediaService mediaService;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentMediaResource.class);
|
||||
|
||||
/**
|
||||
* Récupère tous les médias d'un établissement.
|
||||
*/
|
||||
@GET
|
||||
@Operation(summary = "Récupérer tous les médias d'un établissement",
|
||||
description = "Retourne la liste de tous les médias (photos et vidéos) d'un établissement")
|
||||
public Response getEstablishmentMedia(@PathParam("establishmentId") String establishmentId) {
|
||||
LOG.info("Récupération des médias pour l'établissement : " + establishmentId);
|
||||
|
||||
try {
|
||||
UUID id = UUID.fromString(establishmentId);
|
||||
List<EstablishmentMedia> medias = mediaService.getMediaByEstablishmentId(id);
|
||||
List<EstablishmentMediaResponseDTO> responseDTOs = medias.stream()
|
||||
.map(EstablishmentMediaResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.error("ID d'établissement invalide : " + establishmentId);
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("ID d'établissement invalide")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur lors de la récupération des médias", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la récupération des médias")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload un nouveau média pour un établissement.
|
||||
* Accepte un body JSON avec les informations du média.
|
||||
*/
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Uploader un média pour un établissement",
|
||||
description = "Upload un nouveau média (photo ou vidéo) pour un établissement")
|
||||
public Response uploadMedia(
|
||||
@PathParam("establishmentId") String establishmentId,
|
||||
@Valid EstablishmentMediaRequestDTO requestDTO,
|
||||
@QueryParam("uploadedByUserId") String uploadedByUserIdStr) {
|
||||
LOG.info("Upload d'un média pour l'établissement : " + establishmentId);
|
||||
|
||||
try {
|
||||
UUID id = UUID.fromString(establishmentId);
|
||||
|
||||
// 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(requestDTO.getMediaType().toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("Type de média invalide. Utilisez PHOTO ou VIDEO")
|
||||
.build();
|
||||
}
|
||||
|
||||
// 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(), 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(), e);
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(e.getMessage())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur inattendue lors de l'upload du média", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de l'upload du média")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un média d'un établissement.
|
||||
*/
|
||||
@DELETE
|
||||
@Path("/{mediaId}")
|
||||
@Transactional
|
||||
@Operation(summary = "Supprimer un média d'un établissement",
|
||||
description = "Supprime un média spécifique d'un établissement")
|
||||
public Response deleteMedia(
|
||||
@PathParam("establishmentId") String establishmentId,
|
||||
@PathParam("mediaId") String mediaId) {
|
||||
LOG.info("Suppression du média " + mediaId + " de l'établissement " + establishmentId);
|
||||
|
||||
try {
|
||||
UUID establishmentUuid = UUID.fromString(establishmentId);
|
||||
UUID mediaUuid = UUID.fromString(mediaId);
|
||||
mediaService.deleteMedia(establishmentUuid, mediaUuid);
|
||||
return Response.status(Response.Status.NO_CONTENT).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.error("ID invalide : " + e.getMessage());
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("ID invalide")
|
||||
.build();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("Erreur lors de la suppression du média : " + e.getMessage());
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(e.getMessage())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur inattendue lors de la suppression du média", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la suppression du média")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.dto.request.establishment.EstablishmentRatingRequestDTO;
|
||||
import com.lions.dev.dto.response.establishment.EstablishmentRatingResponseDTO;
|
||||
import com.lions.dev.dto.response.establishment.EstablishmentRatingStatsResponseDTO;
|
||||
import com.lions.dev.entity.establishment.EstablishmentRating;
|
||||
import com.lions.dev.service.EstablishmentRatingService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des notations d'établissements.
|
||||
* Cette classe expose des endpoints pour soumettre, modifier et récupérer les notes.
|
||||
*/
|
||||
@Path("/establishments/{establishmentId}/ratings")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Establishment Ratings", description = "Opérations liées aux notations des établissements")
|
||||
public class EstablishmentRatingResource {
|
||||
|
||||
@Inject
|
||||
EstablishmentRatingService ratingService;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentRatingResource.class);
|
||||
|
||||
/**
|
||||
* Soumet une nouvelle note pour un établissement.
|
||||
*/
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Soumettre une note pour un établissement",
|
||||
description = "Soumet une nouvelle note (1 à 5 étoiles) pour un établissement")
|
||||
public Response submitRating(
|
||||
@PathParam("establishmentId") String establishmentId,
|
||||
@QueryParam("userId") String userIdStr,
|
||||
@Valid EstablishmentRatingRequestDTO requestDTO) {
|
||||
LOG.info("Soumission d'une note pour l'établissement " + establishmentId + " par l'utilisateur " + userIdStr);
|
||||
|
||||
try {
|
||||
UUID id = UUID.fromString(establishmentId);
|
||||
UUID userId = UUID.fromString(userIdStr);
|
||||
|
||||
EstablishmentRating rating = ratingService.submitRating(id, userId, requestDTO);
|
||||
EstablishmentRatingResponseDTO responseDTO = new EstablishmentRatingResponseDTO(rating);
|
||||
return Response.status(Response.Status.CREATED).entity(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 (RuntimeException e) {
|
||||
LOG.error("Erreur lors de la soumission de la note : " + e.getMessage());
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(e.getMessage())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur inattendue lors de la soumission de la note", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la soumission de la note")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une note existante.
|
||||
*/
|
||||
@PUT
|
||||
@Transactional
|
||||
@Operation(summary = "Modifier une note existante",
|
||||
description = "Met à jour une note existante pour un établissement")
|
||||
public Response updateRating(
|
||||
@PathParam("establishmentId") String establishmentId,
|
||||
@QueryParam("userId") String userIdStr,
|
||||
@Valid EstablishmentRatingRequestDTO requestDTO) {
|
||||
LOG.info("Mise à jour de la note pour l'établissement " + establishmentId + " par l'utilisateur " + userIdStr);
|
||||
|
||||
try {
|
||||
UUID id = UUID.fromString(establishmentId);
|
||||
UUID userId = UUID.fromString(userIdStr);
|
||||
|
||||
EstablishmentRating rating = ratingService.updateRating(id, userId, requestDTO);
|
||||
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 (RuntimeException e) {
|
||||
LOG.error("Erreur lors de la mise à jour de la note : " + e.getMessage());
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(e.getMessage())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur inattendue lors de la mise à jour de la note", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la mise à jour 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")
|
||||
@Operation(summary = "Récupérer les statistiques de notation",
|
||||
description = "Récupère les statistiques de notation d'un établissement (moyenne, total, distribution)")
|
||||
public Response getRatingStats(@PathParam("establishmentId") String establishmentId) {
|
||||
LOG.info("Récupération des statistiques de notation pour l'établissement " + establishmentId);
|
||||
|
||||
try {
|
||||
UUID id = UUID.fromString(establishmentId);
|
||||
Map<String, Object> stats = ratingService.getRatingStats(id);
|
||||
|
||||
EstablishmentRatingStatsResponseDTO responseDTO = new EstablishmentRatingStatsResponseDTO(
|
||||
(Double) stats.get("averageRating"),
|
||||
(Integer) stats.get("totalRatingsCount"),
|
||||
(Map<Integer, Integer>) stats.get("ratingDistribution")
|
||||
);
|
||||
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 des statistiques", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la récupération des statistiques")
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
288
src/main/java/com/lions/dev/resource/EstablishmentResource.java
Normal file
288
src/main/java/com/lions/dev/resource/EstablishmentResource.java
Normal file
@@ -0,0 +1,288 @@
|
||||
package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.dto.request.establishment.EstablishmentCreateRequestDTO;
|
||||
import com.lions.dev.dto.request.establishment.EstablishmentUpdateRequestDTO;
|
||||
import com.lions.dev.dto.response.establishment.EstablishmentResponseDTO;
|
||||
import com.lions.dev.entity.establishment.Establishment;
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import com.lions.dev.service.EstablishmentService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des établissements dans le système AfterWork.
|
||||
* Cette classe expose des endpoints pour créer, récupérer, mettre à jour et supprimer des établissements.
|
||||
* Seuls les responsables d'établissement peuvent créer et gérer des établissements.
|
||||
*/
|
||||
@Path("/establishments")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Establishments", description = "Opérations liées à la gestion des établissements")
|
||||
public class EstablishmentResource {
|
||||
|
||||
@Inject
|
||||
EstablishmentService establishmentService;
|
||||
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentResource.class);
|
||||
|
||||
// *********** Création d'un établissement ***********
|
||||
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Créer un nouvel établissement",
|
||||
description = "Crée un nouvel établissement. Seuls les responsables d'établissement peuvent créer des établissements.")
|
||||
public Response createEstablishment(@Valid EstablishmentCreateRequestDTO requestDTO) {
|
||||
LOG.info("[LOG] Tentative de création d'un nouvel établissement : " + requestDTO.getName());
|
||||
|
||||
try {
|
||||
// Vérifier que managerId est fourni
|
||||
if (requestDTO.getManagerId() == null) {
|
||||
LOG.error("[ERROR] managerId est obligatoire pour créer un établissement");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("L'identifiant du responsable (managerId) est obligatoire")
|
||||
.build();
|
||||
}
|
||||
|
||||
// Récupérer le responsable
|
||||
Users manager = usersRepository.findById(requestDTO.getManagerId());
|
||||
if (manager == null) {
|
||||
LOG.error("[ERROR] Responsable non trouvé avec l'ID : " + requestDTO.getManagerId());
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("Responsable non trouvé avec l'ID fourni")
|
||||
.build();
|
||||
}
|
||||
|
||||
// Créer l'établissement
|
||||
Establishment establishment = new Establishment();
|
||||
establishment.setName(requestDTO.getName());
|
||||
establishment.setType(requestDTO.getType());
|
||||
establishment.setAddress(requestDTO.getAddress());
|
||||
establishment.setCity(requestDTO.getCity());
|
||||
establishment.setPostalCode(requestDTO.getPostalCode());
|
||||
establishment.setDescription(requestDTO.getDescription());
|
||||
establishment.setPhoneNumber(requestDTO.getPhoneNumber());
|
||||
establishment.setWebsite(requestDTO.getWebsite());
|
||||
establishment.setPriceRange(requestDTO.getPriceRange());
|
||||
establishment.setLatitude(requestDTO.getLatitude());
|
||||
establishment.setLongitude(requestDTO.getLongitude());
|
||||
|
||||
Establishment createdEstablishment = establishmentService.createEstablishment(establishment, requestDTO.getManagerId());
|
||||
LOG.info("[LOG] Établissement créé avec succès : " + createdEstablishment.getName());
|
||||
|
||||
EstablishmentResponseDTO responseDTO = new EstablishmentResponseDTO(createdEstablishment);
|
||||
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("[ERROR] Erreur lors de la création de l'établissement : " + e.getMessage());
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(e.getMessage())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur inattendue lors de la création de l'établissement", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la création de l'établissement")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// *********** Récupération de tous les établissements ***********
|
||||
|
||||
@GET
|
||||
@Operation(summary = "Récupérer tous les établissements",
|
||||
description = "Retourne la liste de tous les établissements disponibles")
|
||||
public Response getAllEstablishments() {
|
||||
LOG.info("[LOG] Récupération de tous les établissements");
|
||||
try {
|
||||
List<Establishment> establishments = establishmentService.getAllEstablishments();
|
||||
List<EstablishmentResponseDTO> responseDTOs = establishments.stream()
|
||||
.map(EstablishmentResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la récupération des établissements", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la récupération des établissements")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// *********** Récupération d'un établissement par ID ***********
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@Operation(summary = "Récupérer un établissement par ID",
|
||||
description = "Retourne les détails de l'établissement demandé")
|
||||
public Response getEstablishmentById(@PathParam("id") UUID id) {
|
||||
LOG.info("[LOG] Récupération de l'établissement avec l'ID : " + id);
|
||||
try {
|
||||
Establishment establishment = establishmentService.getEstablishmentById(id);
|
||||
EstablishmentResponseDTO responseDTO = new EstablishmentResponseDTO(establishment);
|
||||
return Response.ok(responseDTO).build();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.warn("[WARN] " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(e.getMessage())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la récupération de l'établissement", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la récupération de l'établissement")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// *********** Recherche d'établissements ***********
|
||||
|
||||
@GET
|
||||
@Path("/search")
|
||||
@Operation(summary = "Rechercher des établissements",
|
||||
description = "Recherche des établissements par nom ou ville")
|
||||
public Response searchEstablishments(@QueryParam("q") String query) {
|
||||
LOG.info("[LOG] Recherche d'établissements avec la requête : " + query);
|
||||
try {
|
||||
if (query == null || query.trim().isEmpty()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("Le paramètre de recherche 'q' est obligatoire")
|
||||
.build();
|
||||
}
|
||||
List<Establishment> establishments = establishmentService.searchEstablishments(query);
|
||||
List<EstablishmentResponseDTO> responseDTOs = establishments.stream()
|
||||
.map(EstablishmentResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la recherche d'établissements", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la recherche d'établissements")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// *********** Filtrage d'établissements ***********
|
||||
|
||||
@GET
|
||||
@Path("/filter")
|
||||
@Operation(summary = "Filtrer les établissements",
|
||||
description = "Filtre les établissements par type, fourchette de prix et/ou ville")
|
||||
public Response filterEstablishments(
|
||||
@QueryParam("type") String type,
|
||||
@QueryParam("priceRange") String priceRange,
|
||||
@QueryParam("city") String city) {
|
||||
LOG.info("[LOG] Filtrage des établissements : type=" + type + ", priceRange=" + priceRange + ", city=" + city);
|
||||
try {
|
||||
List<Establishment> establishments = establishmentService.filterEstablishments(type, priceRange, city);
|
||||
List<EstablishmentResponseDTO> responseDTOs = establishments.stream()
|
||||
.map(EstablishmentResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors du filtrage des établissements", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors du filtrage des établissements")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// *********** Récupération des établissements d'un responsable ***********
|
||||
|
||||
@GET
|
||||
@Path("/manager/{managerId}")
|
||||
@Operation(summary = "Récupérer les établissements d'un responsable",
|
||||
description = "Retourne tous les établissements gérés par un responsable")
|
||||
public Response getEstablishmentsByManager(@PathParam("managerId") UUID managerId) {
|
||||
LOG.info("[LOG] Récupération des établissements du responsable : " + managerId);
|
||||
try {
|
||||
List<Establishment> establishments = establishmentService.getEstablishmentsByManager(managerId);
|
||||
List<EstablishmentResponseDTO> responseDTOs = establishments.stream()
|
||||
.map(EstablishmentResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la récupération des établissements du responsable", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la récupération des établissements du responsable")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// *********** Mise à jour d'un établissement ***********
|
||||
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@Operation(summary = "Mettre à jour un établissement",
|
||||
description = "Met à jour les informations d'un établissement existant")
|
||||
public Response updateEstablishment(
|
||||
@PathParam("id") UUID id,
|
||||
@Valid EstablishmentUpdateRequestDTO requestDTO) {
|
||||
LOG.info("[LOG] Mise à jour de l'établissement avec l'ID : " + id);
|
||||
try {
|
||||
Establishment establishment = new Establishment();
|
||||
establishment.setName(requestDTO.getName());
|
||||
establishment.setType(requestDTO.getType());
|
||||
establishment.setAddress(requestDTO.getAddress());
|
||||
establishment.setCity(requestDTO.getCity());
|
||||
establishment.setPostalCode(requestDTO.getPostalCode());
|
||||
establishment.setDescription(requestDTO.getDescription());
|
||||
establishment.setPhoneNumber(requestDTO.getPhoneNumber());
|
||||
establishment.setWebsite(requestDTO.getWebsite());
|
||||
establishment.setPriceRange(requestDTO.getPriceRange());
|
||||
establishment.setLatitude(requestDTO.getLatitude());
|
||||
establishment.setLongitude(requestDTO.getLongitude());
|
||||
|
||||
Establishment updatedEstablishment = establishmentService.updateEstablishment(id, establishment);
|
||||
EstablishmentResponseDTO responseDTO = new EstablishmentResponseDTO(updatedEstablishment);
|
||||
return Response.ok(responseDTO).build();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("[ERROR] " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(e.getMessage())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la mise à jour de l'établissement", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la mise à jour de l'établissement")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// *********** Suppression d'un établissement ***********
|
||||
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@Operation(summary = "Supprimer un établissement",
|
||||
description = "Supprime un établissement du système")
|
||||
public Response deleteEstablishment(@PathParam("id") UUID id) {
|
||||
LOG.info("[LOG] Suppression de l'établissement avec l'ID : " + id);
|
||||
try {
|
||||
establishmentService.deleteEstablishment(id);
|
||||
return Response.noContent().build();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("[ERROR] " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(e.getMessage())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la suppression de l'établissement", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la suppression de l'établissement")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,11 +66,25 @@ public class EventsResource {
|
||||
@Operation(summary = "Créer un nouvel événement", description = "Crée un nouvel événement et retourne ses détails")
|
||||
public Response createEvent(EventCreateRequestDTO eventCreateRequestDTO) {
|
||||
LOG.info("[LOG] Tentative de création d'un nouvel événement : " + eventCreateRequestDTO.getTitle());
|
||||
|
||||
// Valider que creatorId est fourni
|
||||
if (eventCreateRequestDTO.getCreatorId() == null) {
|
||||
LOG.error("[ERROR] creatorId est obligatoire pour créer un événement");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("L'identifiant du créateur (creatorId) est obligatoire")
|
||||
.build();
|
||||
}
|
||||
|
||||
// Récupérer le créateur par son ID
|
||||
Users creator = usersRepository.findById(eventCreateRequestDTO.getCreatorId());
|
||||
if (creator == null) {
|
||||
LOG.error("[ERROR] Créateur non trouvé avec l'ID : " + eventCreateRequestDTO.getCreatorId());
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity("Créateur non trouvé").build();
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("Créateur non trouvé avec l'ID fourni")
|
||||
.build();
|
||||
}
|
||||
|
||||
// Créer l'événement
|
||||
Events event = eventService.createEvent(eventCreateRequestDTO, creator);
|
||||
LOG.info("[LOG] Événement créé avec succès : " + event.getTitle());
|
||||
EventCreateResponseDTO responseDTO = new EventCreateResponseDTO(event);
|
||||
@@ -100,10 +114,16 @@ public class EventsResource {
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@Operation(summary = "Supprimer un événement", description = "Supprime un événement de la base de données")
|
||||
public Response deleteEvent(@PathParam("id") UUID id) {
|
||||
LOG.info("Tentative de suppression de l'événement avec l'ID : " + id);
|
||||
public Response deleteEvent(@PathParam("id") UUID id, @QueryParam("userId") UUID userId) {
|
||||
LOG.info("Tentative de suppression de l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
|
||||
|
||||
if (userId == null) {
|
||||
LOG.error("[ERROR] userId est obligatoire pour supprimer un événement");
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
|
||||
}
|
||||
|
||||
try {
|
||||
boolean deleted = eventService.deleteEvent(id);
|
||||
boolean deleted = eventService.deleteEvent(id, userId);
|
||||
if (deleted) {
|
||||
LOG.info("Événement supprimé avec succès.");
|
||||
return Response.noContent().build();
|
||||
@@ -114,6 +134,9 @@ public class EventsResource {
|
||||
} catch (EventNotFoundException e) {
|
||||
LOG.error("[ERROR] Échec de la suppression : " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build();
|
||||
} catch (SecurityException e) {
|
||||
LOG.error("[ERROR] Permissions insuffisantes : " + e.getMessage());
|
||||
return Response.status(Response.Status.FORBIDDEN).entity(e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,19 +189,31 @@ public class EventsResource {
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@Operation(summary = "Mettre à jour un événement", description = "Modifie un événement existant")
|
||||
public Response updateEvent(@PathParam("id") UUID id, EventUpdateRequestDTO eventUpdateRequestDTO) {
|
||||
LOG.info("[LOG] Tentative de mise à jour de l'événement avec l'ID : " + id);
|
||||
public Response updateEvent(@PathParam("id") UUID id, @QueryParam("userId") UUID userId, EventUpdateRequestDTO eventUpdateRequestDTO) {
|
||||
LOG.info("[LOG] Tentative de mise à jour de l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
|
||||
|
||||
if (userId == null) {
|
||||
LOG.error("[ERROR] userId est obligatoire pour mettre à jour un événement");
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
|
||||
}
|
||||
|
||||
Events event = eventsRepository.findById(id);
|
||||
if (event == null) {
|
||||
LOG.warn("[LOG] Événement non trouvé avec l'ID : " + id);
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build();
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est le créateur
|
||||
if (!eventService.canModifyEvent(event, userId)) {
|
||||
LOG.error("[ERROR] L'utilisateur " + userId + " n'a pas les permissions pour modifier l'événement " + id);
|
||||
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour modifier cet événement").build();
|
||||
}
|
||||
|
||||
// Mise à jour des attributs de l'événement
|
||||
event.setTitle(eventUpdateRequestDTO.getTitle());
|
||||
event.setStartDate(eventUpdateRequestDTO.getStartDate());
|
||||
event.setEndDate(eventUpdateRequestDTO.getEndDate());
|
||||
event.setDescription(eventUpdateRequestDTO.getDescription());
|
||||
event.setLocation(eventUpdateRequestDTO.getLocation());
|
||||
event.setCategory(eventUpdateRequestDTO.getCategory());
|
||||
event.setLink(eventUpdateRequestDTO.getLink());
|
||||
event.setImageUrl(eventUpdateRequestDTO.getImageUrl());
|
||||
@@ -194,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);
|
||||
@@ -206,18 +243,22 @@ public class EventsResource {
|
||||
try {
|
||||
List<FriendshipReadFriendDetailsResponseDTO> friends = friendshipService.listFriends(userId, 0, Integer.MAX_VALUE);
|
||||
Set<UUID> friendIds = friends.stream().map(FriendshipReadFriendDetailsResponseDTO::getFriendId).collect(Collectors.toSet());
|
||||
//friendIds = friendIds.stream().distinct().collect(Collectors.toSet());
|
||||
LOG.info("[LOG] IDs d'amis + utilisateur (taille attendue: " + friendIds.size() + ") : " + friendIds);
|
||||
if (friendIds.isEmpty()) {
|
||||
LOG.warn("[LOG] Aucun ami trouvé.");
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("Aucun ami trouvé.").build();
|
||||
}
|
||||
List<Events> events = eventsRepository.find("creator.id IN ?1", friendIds).list();
|
||||
|
||||
// IMPORTANT: Ajouter l'ID de l'utilisateur lui-même pour inclure ses propres événements
|
||||
friendIds.add(userId);
|
||||
|
||||
LOG.info("[LOG] IDs d'amis + utilisateur (taille: " + friendIds.size() + ") : " + friendIds);
|
||||
|
||||
// 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());
|
||||
if (events.isEmpty()) {
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("Aucun événement trouvé.").build();
|
||||
}
|
||||
List<EventReadManyByIdResponseDTO> responseDTOs = events.stream().map(EventReadManyByIdResponseDTO::new).toList();
|
||||
|
||||
// ✅ Retourner avec reactionsCount et isFavorite pour l'utilisateur actuel
|
||||
List<EventCreateResponseDTO> responseDTOs = events.stream()
|
||||
.map(event -> new EventCreateResponseDTO(event, usersRepository, userId))
|
||||
.toList();
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur de récupération des événements : ", e);
|
||||
@@ -366,13 +407,26 @@ public class EventsResource {
|
||||
@Operation(
|
||||
summary = "Mettre à jour le statut d'un événement",
|
||||
description = "Modifie le statut d'un événement (ouvert, fermé, annulé, etc.)")
|
||||
public Response updateEventStatus(@PathParam("id") UUID id, @QueryParam("status") String status) {
|
||||
LOG.info("[LOG] Mise à jour du statut de l'événement avec l'ID : " + id);
|
||||
public Response updateEventStatus(@PathParam("id") UUID id, @QueryParam("status") String status, @QueryParam("userId") UUID userId) {
|
||||
LOG.info("[LOG] Mise à jour du statut de l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
|
||||
|
||||
if (userId == null) {
|
||||
LOG.error("[ERROR] userId est obligatoire pour mettre à jour le statut d'un événement");
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
|
||||
}
|
||||
|
||||
Events event = eventsRepository.findById(id);
|
||||
if (event == null) {
|
||||
LOG.warn("[LOG] Événement non trouvé avec l'ID : " + id);
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build();
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est le créateur
|
||||
if (!eventService.canModifyEvent(event, userId)) {
|
||||
LOG.error("[ERROR] L'utilisateur " + userId + " n'a pas les permissions pour modifier le statut de l'événement " + id);
|
||||
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour modifier cet événement").build();
|
||||
}
|
||||
|
||||
event.setStatus(status);
|
||||
eventsRepository.persist(event);
|
||||
LOG.info("[LOG] Statut de l'événement mis à jour avec succès : " + status);
|
||||
@@ -393,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();
|
||||
@@ -423,8 +480,13 @@ public class EventsResource {
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/image")
|
||||
public Response updateEventImage(@PathParam("id") UUID id, String imageFilePath) {
|
||||
LOG.info("[LOG] Tentative de mise à jour de l'image pour l'événement avec l'ID : " + id);
|
||||
public Response updateEventImage(@PathParam("id") UUID id, @QueryParam("userId") UUID userId, String imageFilePath) {
|
||||
LOG.info("[LOG] Tentative de mise à jour de l'image pour l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
|
||||
|
||||
if (userId == null) {
|
||||
LOG.error("[ERROR] userId est obligatoire pour mettre à jour l'image d'un événement");
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
|
||||
}
|
||||
|
||||
try {
|
||||
if (imageFilePath == null || imageFilePath.isEmpty()) {
|
||||
@@ -444,9 +506,15 @@ public class EventsResource {
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build();
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est le créateur
|
||||
if (!eventService.canModifyEvent(event, userId)) {
|
||||
LOG.error("[ERROR] L'utilisateur " + userId + " n'a pas les permissions pour modifier l'image de l'événement " + id);
|
||||
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour modifier cet événement").build();
|
||||
}
|
||||
|
||||
String imageUrl = file.getAbsolutePath();
|
||||
event.setImageUrl(imageUrl);
|
||||
eventService.updateEvent(event);
|
||||
eventService.updateEvent(event, userId);
|
||||
|
||||
LOG.info("[LOG] Image de l'événement mise à jour avec succès pour : " + event.getTitle());
|
||||
return Response.ok("Image de l'événement mise à jour avec succès.").build();
|
||||
@@ -461,8 +529,13 @@ public class EventsResource {
|
||||
@Path("/{id}/partial-update")
|
||||
@Transactional
|
||||
@Operation(summary = "Mettre à jour partiellement un événement", description = "Mise à jour partielle des informations d'un événement")
|
||||
public Response partialUpdateEvent(@PathParam("id") UUID id, Map<String, Object> updates) {
|
||||
LOG.info("[LOG] Tentative de mise à jour partielle de l'événement avec l'ID : " + id);
|
||||
public Response partialUpdateEvent(@PathParam("id") UUID id, @QueryParam("userId") UUID userId, Map<String, Object> updates) {
|
||||
LOG.info("[LOG] Tentative de mise à jour partielle de l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
|
||||
|
||||
if (userId == null) {
|
||||
LOG.error("[ERROR] userId est obligatoire pour mettre à jour un événement");
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
|
||||
}
|
||||
|
||||
Events event = eventsRepository.findById(id);
|
||||
if (event == null) {
|
||||
@@ -470,9 +543,36 @@ public class EventsResource {
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build();
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est le créateur
|
||||
if (!eventService.canModifyEvent(event, userId)) {
|
||||
LOG.error("[ERROR] L'utilisateur " + userId + " n'a pas les permissions pour modifier l'événement " + id);
|
||||
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour modifier cet événement").build();
|
||||
}
|
||||
|
||||
// Mise à jour des champs dynamiquement
|
||||
updates.forEach((field, value) -> {
|
||||
// Mise à jour des champs dynamiquement, à adapter selon la structure de l'événement
|
||||
// Exemple d'utilisation de réflexion si applicable
|
||||
switch (field) {
|
||||
case "title":
|
||||
event.setTitle(value != null ? value.toString() : null);
|
||||
break;
|
||||
case "description":
|
||||
event.setDescription(value != null ? value.toString() : null);
|
||||
break;
|
||||
case "category":
|
||||
event.setCategory(value != null ? value.toString() : null);
|
||||
break;
|
||||
case "link":
|
||||
event.setLink(value != null ? value.toString() : null);
|
||||
break;
|
||||
case "imageUrl":
|
||||
event.setImageUrl(value != null ? value.toString() : null);
|
||||
break;
|
||||
case "status":
|
||||
event.setStatus(value != null ? value.toString() : null);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("[LOG] Champ inconnu ignoré lors de la mise à jour partielle : " + field);
|
||||
}
|
||||
});
|
||||
|
||||
eventsRepository.persist(event);
|
||||
@@ -519,8 +619,13 @@ public class EventsResource {
|
||||
@Path("/{id}/cancel")
|
||||
@Transactional
|
||||
@Operation(summary = "Annuler un événement", description = "Annule un événement sans le supprimer.")
|
||||
public Response cancelEvent(@PathParam("id") UUID id) {
|
||||
LOG.info("[LOG] Annulation de l'événement avec l'ID : " + id);
|
||||
public Response cancelEvent(@PathParam("id") UUID id, @QueryParam("userId") UUID userId) {
|
||||
LOG.info("[LOG] Annulation de l'événement avec l'ID : " + id + " par l'utilisateur : " + userId);
|
||||
|
||||
if (userId == null) {
|
||||
LOG.error("[ERROR] userId est obligatoire pour annuler un événement");
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity("L'identifiant de l'utilisateur (userId) est obligatoire").build();
|
||||
}
|
||||
|
||||
Events event = eventsRepository.findById(id);
|
||||
if (event == null) {
|
||||
@@ -528,6 +633,12 @@ public class EventsResource {
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build();
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est le créateur
|
||||
if (!eventService.canModifyEvent(event, userId)) {
|
||||
LOG.error("[ERROR] L'utilisateur " + userId + " n'a pas les permissions pour annuler l'événement " + id);
|
||||
return Response.status(Response.Status.FORBIDDEN).entity("Vous n'avez pas les permissions pour annuler cet événement").build();
|
||||
}
|
||||
|
||||
event.setStatus("Annulé");
|
||||
eventsRepository.persist(event);
|
||||
LOG.info("[LOG] Événement annulé avec succès : " + event.getTitle());
|
||||
@@ -654,9 +765,9 @@ public class EventsResource {
|
||||
@POST
|
||||
@Path("/{id}/favorite")
|
||||
@Transactional
|
||||
@Operation(summary = "Marquer un événement comme favori", description = "Permet à un utilisateur de marquer un événement comme favori.")
|
||||
@Operation(summary = "Toggle favori d'un événement", description = "Permet à un utilisateur d'ajouter ou retirer un événement de ses favoris (toggle).")
|
||||
public Response favoriteEvent(@PathParam("id") UUID eventId, @QueryParam("userId") UUID userId) {
|
||||
LOG.info("[LOG] Marquage de l'événement comme favori pour l'utilisateur ID : " + userId);
|
||||
LOG.info("[LOG] Toggle favori de l'événement " + eventId + " pour l'utilisateur ID : " + userId);
|
||||
|
||||
Events event = eventsRepository.findById(eventId);
|
||||
Users user = usersRepository.findById(userId);
|
||||
@@ -665,9 +776,22 @@ public class EventsResource {
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("Événement ou utilisateur non trouvé.").build();
|
||||
}
|
||||
|
||||
user.addFavoriteEvent(event);
|
||||
// ✅ Toggle : ajouter si pas favori, retirer si déjà favori
|
||||
boolean wasFavorite = user.hasFavoriteEvent(event);
|
||||
if (wasFavorite) {
|
||||
user.removeFavoriteEvent(event);
|
||||
LOG.info("[LOG] Événement retiré des favoris pour l'utilisateur : " + user.getEmail());
|
||||
} else {
|
||||
user.addFavoriteEvent(event);
|
||||
LOG.info("[LOG] Événement ajouté aux favoris pour l'utilisateur : " + user.getEmail());
|
||||
}
|
||||
usersRepository.persist(user);
|
||||
return Response.ok("Événement marqué comme favori.").build();
|
||||
|
||||
// Retourner un JSON avec le statut
|
||||
Map<String, Object> response = new java.util.HashMap<>();
|
||||
response.put("isFavorite", !wasFavorite);
|
||||
response.put("message", wasFavorite ? "Événement retiré des favoris." : "Événement ajouté aux favoris.");
|
||||
return Response.ok(response).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -716,6 +840,50 @@ public class EventsResource {
|
||||
return Response.ok(responseDTOs).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{id}/comments")
|
||||
@Transactional
|
||||
@Operation(summary = "Ajouter un commentaire à un événement", description = "Crée un nouveau commentaire pour un événement.")
|
||||
public Response addComment(
|
||||
@PathParam("id") UUID eventId,
|
||||
@QueryParam("userId") UUID userId,
|
||||
Map<String, String> requestBody) {
|
||||
LOG.info("[LOG] Ajout d'un commentaire à l'événement ID : " + eventId + " par l'utilisateur ID : " + userId);
|
||||
|
||||
Events event = eventsRepository.findById(eventId);
|
||||
if (event == null) {
|
||||
LOG.warn("[LOG] Événement non trouvé avec l'ID : " + eventId);
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
String text = requestBody.get("text");
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
LOG.warn("[LOG] Le texte du commentaire est vide");
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity("Le texte du commentaire est requis.").build();
|
||||
}
|
||||
|
||||
try {
|
||||
Comment comment = new Comment(user, event, text);
|
||||
// Le commentaire sera automatiquement ajouté à la liste des commentaires de l'événement via la relation JPA
|
||||
eventsRepository.persist(event);
|
||||
|
||||
LOG.info("[LOG] Commentaire ajouté avec succès à l'événement : " + event.getTitle());
|
||||
|
||||
CommentResponseDTO responseDTO = new CommentResponseDTO(comment);
|
||||
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de l'ajout du commentaire : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de l'ajout du commentaire.").build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/recommendations/{userId}")
|
||||
@Operation(summary = "Recommander des événements basés sur les intérêts", description = "Retourne une liste d'événements recommandés pour un utilisateur.")
|
||||
@@ -850,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 {
|
||||
|
||||
@@ -2,17 +2,25 @@ package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.service.FileService;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.reactive.RestForm;
|
||||
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
||||
import jakarta.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Path("/upload")
|
||||
@Path("/media")
|
||||
public class FileUploadResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(FileUploadResource.class);
|
||||
@@ -20,17 +28,208 @@ public class FileUploadResource {
|
||||
@Inject
|
||||
FileService fileService;
|
||||
|
||||
@Context
|
||||
UriInfo uriInfo;
|
||||
|
||||
@POST
|
||||
@Path("/upload")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response uploadFile(@RestForm("file") FileUpload file) {
|
||||
String uploadDir = "/tmp/uploads/";
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response uploadFile(
|
||||
@RestForm("file") FileUpload file,
|
||||
@RestForm("type") String type,
|
||||
@RestForm("fileName") String fileName,
|
||||
@RestForm("contentType") String contentType,
|
||||
@RestForm("fileSize") String fileSize,
|
||||
@RestForm("userId") String userId) {
|
||||
|
||||
LOG.info("Début de l'upload de fichier");
|
||||
LOG.infof("Type: %s, FileName: %s, ContentType: %s, FileSize: %s, UserId: %s",
|
||||
type, fileName, contentType, fileSize, userId);
|
||||
|
||||
if (file == null || file.uploadedFile() == null) {
|
||||
LOG.error("Aucun fichier fourni dans la requête");
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(createErrorResponse("Aucun fichier fourni"))
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
Path savedFilePath = (jakarta.ws.rs.Path) fileService.saveFile(file.uploadedFile(), uploadDir, file.fileName());
|
||||
return Response.ok("Fichier uploadé avec succès : " + savedFilePath).build();
|
||||
// Générer un nom de fichier unique si nécessaire
|
||||
String finalFileName = fileName != null && !fileName.isEmpty()
|
||||
? fileName
|
||||
: generateUniqueFileName(file.fileName());
|
||||
|
||||
// Déterminer le type de média
|
||||
String mediaType = type != null && !type.isEmpty()
|
||||
? type
|
||||
: determineMediaType(file.fileName());
|
||||
|
||||
// Répertoire d'upload
|
||||
String uploadDir = "/tmp/uploads/";
|
||||
|
||||
// Sauvegarder le fichier
|
||||
java.nio.file.Path savedFilePath = fileService.saveFile(
|
||||
file.uploadedFile(),
|
||||
uploadDir,
|
||||
finalFileName
|
||||
);
|
||||
|
||||
LOG.infof("Fichier sauvegardé avec succès: %s", savedFilePath);
|
||||
|
||||
// Construire l'URL du fichier
|
||||
// Note: En production, vous devriez utiliser une URL publique (CDN, S3, etc.)
|
||||
String fileUrl = buildFileUrl(finalFileName, uriInfo);
|
||||
String thumbnailUrl = null;
|
||||
|
||||
// Pour les vidéos, on pourrait générer un thumbnail ici
|
||||
if ("video".equalsIgnoreCase(mediaType)) {
|
||||
// TODO: Générer un thumbnail pour les vidéos
|
||||
thumbnailUrl = fileUrl; // Placeholder
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
response.put("type", mediaType);
|
||||
if (fileSize != null && !fileSize.isEmpty()) {
|
||||
try {
|
||||
long size = Long.parseLong(fileSize);
|
||||
// Pour les vidéos, on pourrait calculer la durée ici
|
||||
// response.put("duration", durationInSeconds);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warnf("Impossible de parser fileSize: %s", fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
LOG.infof("Upload réussi, URL: %s, FileName: %s", fileUrl, finalFileName);
|
||||
|
||||
return Response.status(Response.Status.CREATED)
|
||||
.entity(response)
|
||||
.build();
|
||||
|
||||
} catch (IOException e) {
|
||||
LOG.error("Erreur lors de l'upload du fichier", e);
|
||||
return Response.serverError().entity("Erreur lors de l'upload du fichier.").build();
|
||||
LOG.errorf(e, "Erreur lors de l'upload du fichier: %s", e.getMessage());
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(createErrorResponse("Erreur lors de l'upload du fichier: " + e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur inattendue lors de l'upload: %s", e.getMessage());
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(createErrorResponse("Erreur inattendue."))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un nom de fichier unique.
|
||||
*/
|
||||
private String generateUniqueFileName(String originalFileName) {
|
||||
String extension = "";
|
||||
int lastDot = originalFileName.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
extension = originalFileName.substring(lastDot);
|
||||
}
|
||||
return UUID.randomUUID().toString() + extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine le type de média basé sur l'extension du fichier.
|
||||
*/
|
||||
private String determineMediaType(String fileName) {
|
||||
if (fileName == null) {
|
||||
return "image";
|
||||
}
|
||||
String lowerFileName = fileName.toLowerCase();
|
||||
if (lowerFileName.endsWith(".mp4") ||
|
||||
lowerFileName.endsWith(".mov") ||
|
||||
lowerFileName.endsWith(".avi") ||
|
||||
lowerFileName.endsWith(".mkv") ||
|
||||
lowerFileName.endsWith(".m4v")) {
|
||||
return "video";
|
||||
}
|
||||
return "image";
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit l'URL du fichier.
|
||||
* En production, cela devrait pointer vers un CDN ou un service de stockage.
|
||||
*/
|
||||
private String buildFileUrl(String fileName, UriInfo uriInfo) {
|
||||
// Construire l'URL de base à partir de l'URI de la requête
|
||||
String baseUri = uriInfo.getBaseUri().toString();
|
||||
// Retirer le slash final s'il existe
|
||||
if (baseUri.endsWith("/")) {
|
||||
baseUri = baseUri.substring(0, baseUri.length() - 1);
|
||||
}
|
||||
// Retourner une URL absolue
|
||||
return baseUri + "/media/files/" + fileName;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/files/{fileName}")
|
||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||
public Response getFile(@PathParam("fileName") String fileName) {
|
||||
try {
|
||||
java.nio.file.Path filePath = java.nio.file.Paths.get("/tmp/uploads/", fileName);
|
||||
|
||||
if (!java.nio.file.Files.exists(filePath)) {
|
||||
LOG.warnf("Fichier non trouvé: %s", fileName);
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(createErrorResponse("Fichier non trouvé"))
|
||||
.build();
|
||||
}
|
||||
|
||||
// Déterminer le content-type
|
||||
String contentType = determineContentType(fileName);
|
||||
|
||||
return Response.ok(filePath.toFile())
|
||||
.type(contentType)
|
||||
.header("Content-Disposition", "inline; filename=\"" + fileName + "\"")
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la récupération du fichier: %s", fileName);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(createErrorResponse("Erreur lors de la récupération du fichier"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine le content-type basé sur l'extension du fichier.
|
||||
*/
|
||||
private String determineContentType(String fileName) {
|
||||
if (fileName == null) {
|
||||
return MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
String lowerFileName = fileName.toLowerCase();
|
||||
if (lowerFileName.endsWith(".jpg") || lowerFileName.endsWith(".jpeg")) {
|
||||
return "image/jpeg";
|
||||
} else if (lowerFileName.endsWith(".png")) {
|
||||
return "image/png";
|
||||
} else if (lowerFileName.endsWith(".gif")) {
|
||||
return "image/gif";
|
||||
} else if (lowerFileName.endsWith(".mp4")) {
|
||||
return "video/mp4";
|
||||
} else if (lowerFileName.endsWith(".mov")) {
|
||||
return "video/quicktime";
|
||||
} else if (lowerFileName.endsWith(".avi")) {
|
||||
return "video/x-msvideo";
|
||||
}
|
||||
return MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une réponse d'erreur JSON.
|
||||
*/
|
||||
private Map<String, String> createErrorResponse(String message) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("error", message);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.entity.establishment.Establishment;
|
||||
import com.lions.dev.entity.establishment.EstablishmentMedia;
|
||||
import com.lions.dev.entity.establishment.MediaType;
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.repository.EstablishmentMediaRepository;
|
||||
import com.lions.dev.repository.EstablishmentRepository;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service de gestion des médias d'établissements.
|
||||
* Ce service contient la logique métier pour l'upload, la récupération et la suppression des médias.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class EstablishmentMediaService {
|
||||
|
||||
@Inject
|
||||
EstablishmentMediaRepository mediaRepository;
|
||||
|
||||
@Inject
|
||||
EstablishmentRepository establishmentRepository;
|
||||
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentMediaService.class);
|
||||
|
||||
/**
|
||||
* Récupère tous les médias d'un établissement.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @return Liste des médias de l'établissement
|
||||
*/
|
||||
public List<EstablishmentMedia> getMediaByEstablishmentId(UUID establishmentId) {
|
||||
LOG.info("Récupération des médias pour l'établissement : " + establishmentId);
|
||||
return mediaRepository.findByEstablishmentId(establishmentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload un nouveau média pour un établissement.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @param mediaUrl L'URL du média
|
||||
* @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, Integer displayOrder) {
|
||||
LOG.info("Upload d'un média pour l'établissement : " + establishmentId);
|
||||
|
||||
// Vérifier que l'établissement existe
|
||||
Establishment establishment = establishmentRepository.findById(establishmentId);
|
||||
if (establishment == null) {
|
||||
LOG.error("Établissement non trouvé avec l'ID : " + establishmentId);
|
||||
throw new RuntimeException("Établissement non trouvé");
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur existe
|
||||
Users uploadedBy = usersRepository.findById(uploadedByUserId);
|
||||
if (uploadedBy == null) {
|
||||
LOG.error("Utilisateur non trouvé avec l'ID : " + uploadedByUserId);
|
||||
throw new RuntimeException("Utilisateur non trouvé");
|
||||
}
|
||||
|
||||
// Créer le média
|
||||
EstablishmentMedia media = new EstablishmentMedia(establishment, mediaUrl, mediaType, uploadedBy);
|
||||
media.setThumbnailUrl(thumbnailUrl);
|
||||
|
||||
// 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());
|
||||
return media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un média d'un établissement.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @param mediaId L'ID du média à supprimer
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteMedia(UUID establishmentId, UUID mediaId) {
|
||||
LOG.info("Suppression du média " + mediaId + " de l'établissement " + establishmentId);
|
||||
|
||||
EstablishmentMedia media = mediaRepository.findById(mediaId);
|
||||
if (media == null) {
|
||||
LOG.error("Média non trouvé avec l'ID : " + mediaId);
|
||||
throw new RuntimeException("Média non trouvé");
|
||||
}
|
||||
|
||||
// Vérifier que le média appartient à l'établissement
|
||||
if (!media.getEstablishment().getId().equals(establishmentId)) {
|
||||
LOG.error("Le média " + mediaId + " n'appartient pas à l'établissement " + establishmentId);
|
||||
throw new RuntimeException("Le média n'appartient pas à cet établissement");
|
||||
}
|
||||
|
||||
mediaRepository.delete(media);
|
||||
LOG.info("Média supprimé avec succès : " + mediaId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.dto.request.establishment.EstablishmentRatingRequestDTO;
|
||||
import com.lions.dev.entity.establishment.Establishment;
|
||||
import com.lions.dev.entity.establishment.EstablishmentRating;
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.repository.EstablishmentRatingRepository;
|
||||
import com.lions.dev.repository.EstablishmentRepository;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service de gestion des notations d'établissements.
|
||||
* Ce service contient la logique métier pour soumettre, modifier et récupérer les notes.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class EstablishmentRatingService {
|
||||
|
||||
@Inject
|
||||
EstablishmentRatingRepository ratingRepository;
|
||||
|
||||
@Inject
|
||||
EstablishmentRepository establishmentRepository;
|
||||
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentRatingService.class);
|
||||
|
||||
/**
|
||||
* Soumet une nouvelle note pour un établissement.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @param userId L'ID de l'utilisateur
|
||||
* @param requestDTO Le DTO contenant la note et le commentaire
|
||||
* @return La note créée
|
||||
*/
|
||||
@Transactional
|
||||
public EstablishmentRating submitRating(UUID establishmentId, UUID userId, EstablishmentRatingRequestDTO requestDTO) {
|
||||
LOG.info("Soumission d'une note pour l'établissement " + establishmentId + " par l'utilisateur " + userId);
|
||||
|
||||
// Vérifier que l'établissement existe
|
||||
Establishment establishment = establishmentRepository.findById(establishmentId);
|
||||
if (establishment == null) {
|
||||
LOG.error("Établissement non trouvé avec l'ID : " + establishmentId);
|
||||
throw new RuntimeException("Établissement non trouvé");
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur existe
|
||||
Users user = usersRepository.findById(userId);
|
||||
if (user == null) {
|
||||
LOG.error("Utilisateur non trouvé avec l'ID : " + userId);
|
||||
throw new RuntimeException("Utilisateur non trouvé");
|
||||
}
|
||||
|
||||
// Vérifier qu'il n'y a pas déjà une note de cet utilisateur
|
||||
EstablishmentRating existingRating = ratingRepository.findByEstablishmentIdAndUserId(establishmentId, userId);
|
||||
if (existingRating != null) {
|
||||
LOG.error("L'utilisateur " + userId + " a déjà noté l'établissement " + establishmentId);
|
||||
throw new RuntimeException("Vous avez déjà noté cet établissement. Utilisez la mise à jour pour modifier votre note.");
|
||||
}
|
||||
|
||||
// Créer la note
|
||||
EstablishmentRating rating = new EstablishmentRating(establishment, user, requestDTO.getRating());
|
||||
rating.setComment(requestDTO.getComment());
|
||||
ratingRepository.persist(rating);
|
||||
|
||||
// Mettre à jour les statistiques de l'établissement
|
||||
updateEstablishmentRatingStats(establishmentId);
|
||||
|
||||
LOG.info("Note soumise avec succès : " + rating.getId());
|
||||
return rating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une note existante.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @param userId L'ID de l'utilisateur
|
||||
* @param requestDTO Le DTO contenant la nouvelle note et le commentaire
|
||||
* @return La note mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public EstablishmentRating updateRating(UUID establishmentId, UUID userId, EstablishmentRatingRequestDTO requestDTO) {
|
||||
LOG.info("Mise à jour de la note pour l'établissement " + establishmentId + " par l'utilisateur " + userId);
|
||||
|
||||
// Récupérer la note existante
|
||||
EstablishmentRating rating = ratingRepository.findByEstablishmentIdAndUserId(establishmentId, userId);
|
||||
if (rating == null) {
|
||||
LOG.error("Note non trouvée pour l'établissement " + establishmentId + " et l'utilisateur " + userId);
|
||||
throw new RuntimeException("Note non trouvée. Utilisez la soumission pour créer une nouvelle note.");
|
||||
}
|
||||
|
||||
// Mettre à jour la note
|
||||
rating.updateRating(requestDTO.getRating(), requestDTO.getComment());
|
||||
ratingRepository.persist(rating);
|
||||
|
||||
// Mettre à jour les statistiques de l'établissement
|
||||
updateEstablishmentRatingStats(establishmentId);
|
||||
|
||||
LOG.info("Note mise à jour avec succès : " + rating.getId());
|
||||
return rating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la note d'un utilisateur pour un établissement.
|
||||
*
|
||||
* @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 getUserRating(UUID establishmentId, UUID userId) {
|
||||
LOG.info("Récupération de la note de l'utilisateur " + userId + " pour l'établissement " + establishmentId);
|
||||
return ratingRepository.findByEstablishmentIdAndUserId(establishmentId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les statistiques de notation d'un établissement.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
* @return Map contenant averageRating, totalRatings et distribution
|
||||
*/
|
||||
public Map<String, Object> getRatingStats(UUID establishmentId) {
|
||||
LOG.info("Récupération des statistiques de notation pour l'établissement " + establishmentId);
|
||||
|
||||
Double averageRating = ratingRepository.calculateAverageRating(establishmentId);
|
||||
Long totalRatings = ratingRepository.countByEstablishmentId(establishmentId);
|
||||
Map<Integer, Integer> distribution = ratingRepository.calculateRatingDistribution(establishmentId);
|
||||
|
||||
return Map.of(
|
||||
"averageRating", averageRating,
|
||||
"totalRatingsCount", totalRatings.intValue(),
|
||||
"ratingDistribution", distribution
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les statistiques de notation d'un établissement.
|
||||
* Cette méthode est appelée après chaque création/modification de note.
|
||||
* Note: Cette méthode est appelée depuis des méthodes déjà transactionnelles,
|
||||
* donc pas besoin de l'annotation @Transactional ici.
|
||||
*
|
||||
* @param establishmentId L'ID de l'établissement
|
||||
*/
|
||||
private void updateEstablishmentRatingStats(UUID establishmentId) {
|
||||
Establishment establishment = establishmentRepository.findById(establishmentId);
|
||||
if (establishment == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Double averageRating = ratingRepository.calculateAverageRating(establishmentId);
|
||||
Long totalRatings = ratingRepository.countByEstablishmentId(establishmentId);
|
||||
|
||||
establishment.setAverageRating(averageRating);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
207
src/main/java/com/lions/dev/service/EstablishmentService.java
Normal file
207
src/main/java/com/lions/dev/service/EstablishmentService.java
Normal file
@@ -0,0 +1,207 @@
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.entity.establishment.Establishment;
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.repository.EstablishmentRepository;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service de gestion des établissements.
|
||||
* Ce service contient la logique métier pour la création, récupération,
|
||||
* mise à jour et suppression des établissements.
|
||||
* Seuls les responsables d'établissement peuvent créer et gérer des établissements.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class EstablishmentService {
|
||||
|
||||
@Inject
|
||||
EstablishmentRepository establishmentRepository;
|
||||
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentService.class);
|
||||
|
||||
/**
|
||||
* Crée un nouvel établissement.
|
||||
*
|
||||
* @param establishment L'établissement à créer.
|
||||
* @param managerId L'ID du responsable de l'établissement.
|
||||
* @return L'établissement créé.
|
||||
*/
|
||||
@Transactional
|
||||
public Establishment createEstablishment(Establishment establishment, UUID managerId) {
|
||||
LOG.info("[LOG] Création d'un nouvel établissement : " + establishment.getName());
|
||||
|
||||
// Vérifier que le manager existe
|
||||
Users manager = usersRepository.findById(managerId);
|
||||
if (manager == null) {
|
||||
LOG.error("[ERROR] Responsable non trouvé avec l'ID : " + managerId);
|
||||
throw new RuntimeException("Responsable non trouvé avec l'ID fourni");
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur a le rôle de responsable d'établissement
|
||||
String role = manager.getRole() != null ? manager.getRole().toUpperCase() : "";
|
||||
if (!role.equals("ESTABLISHMENT_MANAGER") &&
|
||||
!role.equals("MANAGER") &&
|
||||
!role.equals("ADMIN")) {
|
||||
LOG.error("[ERROR] L'utilisateur " + managerId + " n'a pas les droits pour créer un établissement. Rôle : " + role);
|
||||
throw new RuntimeException("Seuls les responsables d'établissement peuvent créer des établissements");
|
||||
}
|
||||
|
||||
establishment.setManager(manager);
|
||||
establishmentRepository.persist(establishment);
|
||||
LOG.info("[LOG] Établissement créé avec succès : " + establishment.getName());
|
||||
return establishment;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un établissement par son ID.
|
||||
*
|
||||
* @param id L'ID de l'établissement.
|
||||
* @return L'établissement trouvé.
|
||||
*/
|
||||
public Establishment getEstablishmentById(UUID id) {
|
||||
LOG.info("[LOG] Récupération de l'établissement avec l'ID : " + id);
|
||||
Establishment establishment = establishmentRepository.findById(id);
|
||||
if (establishment == null) {
|
||||
LOG.warn("[WARN] Établissement non trouvé avec l'ID : " + id);
|
||||
throw new RuntimeException("Établissement non trouvé avec l'ID : " + id);
|
||||
}
|
||||
return establishment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un établissement existant.
|
||||
*
|
||||
* @param id L'ID de l'établissement à mettre à jour.
|
||||
* @param updatedEstablishment L'établissement avec les nouvelles données.
|
||||
* @return L'établissement mis à jour.
|
||||
*/
|
||||
@Transactional
|
||||
public Establishment updateEstablishment(UUID id, Establishment updatedEstablishment) {
|
||||
LOG.info("[LOG] Mise à jour de l'établissement avec l'ID : " + id);
|
||||
Establishment establishment = establishmentRepository.findById(id);
|
||||
if (establishment == null) {
|
||||
LOG.error("[ERROR] Établissement non trouvé avec l'ID : " + id);
|
||||
throw new RuntimeException("Établissement non trouvé avec l'ID : " + id);
|
||||
}
|
||||
|
||||
// v2.0 - Mettre à jour les champs
|
||||
establishment.setName(updatedEstablishment.getName());
|
||||
establishment.setType(updatedEstablishment.getType());
|
||||
establishment.setAddress(updatedEstablishment.getAddress());
|
||||
establishment.setCity(updatedEstablishment.getCity());
|
||||
establishment.setPostalCode(updatedEstablishment.getPostalCode());
|
||||
establishment.setDescription(updatedEstablishment.getDescription());
|
||||
establishment.setPhoneNumber(updatedEstablishment.getPhoneNumber());
|
||||
establishment.setWebsite(updatedEstablishment.getWebsite());
|
||||
establishment.setPriceRange(updatedEstablishment.getPriceRange());
|
||||
|
||||
// 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());
|
||||
return establishment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un établissement.
|
||||
*
|
||||
* @param id L'ID de l'établissement à supprimer.
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteEstablishment(UUID id) {
|
||||
LOG.info("[LOG] Suppression de l'établissement avec l'ID : " + id);
|
||||
Establishment establishment = establishmentRepository.findById(id);
|
||||
if (establishment == null) {
|
||||
LOG.error("[ERROR] Établissement non trouvé avec l'ID : " + id);
|
||||
throw new RuntimeException("Établissement non trouvé avec l'ID : " + id);
|
||||
}
|
||||
establishmentRepository.delete(establishment);
|
||||
LOG.info("[LOG] Établissement supprimé avec succès : " + establishment.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les établissements gérés par un responsable.
|
||||
*
|
||||
* @param managerId L'ID du responsable.
|
||||
* @return Une liste d'établissements gérés par ce responsable.
|
||||
*/
|
||||
public List<Establishment> getEstablishmentsByManager(UUID managerId) {
|
||||
LOG.info("[LOG] Récupération des établissements du responsable : " + managerId);
|
||||
List<Establishment> establishments = establishmentRepository.findByManagerId(managerId);
|
||||
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
|
||||
return establishments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche des établissements par nom ou ville.
|
||||
*
|
||||
* @param query Le terme de recherche.
|
||||
* @return Une liste d'établissements correspondant à la recherche.
|
||||
*/
|
||||
public List<Establishment> searchEstablishments(String query) {
|
||||
LOG.info("[LOG] Recherche d'établissements avec la requête : " + query);
|
||||
List<Establishment> establishments = establishmentRepository.searchByNameOrCity(query);
|
||||
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
|
||||
return establishments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtre les établissements par type, fourchette de prix et/ou ville.
|
||||
*
|
||||
* @param type Le type d'établissement (optionnel).
|
||||
* @param priceRange La fourchette de prix (optionnel).
|
||||
* @param city La ville (optionnel).
|
||||
* @return Une liste d'établissements correspondant aux filtres.
|
||||
*/
|
||||
public List<Establishment> filterEstablishments(String type, String priceRange, String city) {
|
||||
LOG.info("[LOG] Filtrage des établissements : type=" + type + ", priceRange=" + priceRange + ", city=" + city);
|
||||
List<Establishment> allEstablishments = establishmentRepository.listAll();
|
||||
|
||||
return allEstablishments.stream()
|
||||
.filter(e -> type == null || e.getType().equalsIgnoreCase(type))
|
||||
.filter(e -> priceRange == null || (e.getPriceRange() != null && e.getPriceRange().equalsIgnoreCase(priceRange)))
|
||||
.filter(e -> city == null || e.getCity().equalsIgnoreCase(city))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,21 +70,52 @@ 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());
|
||||
event.setMaxParticipants(eventCreateRequestDTO.getMaxParticipants());
|
||||
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());
|
||||
event.setAccessibilityInfo(eventCreateRequestDTO.getAccessibilityInfo());
|
||||
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)
|
||||
@@ -79,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,
|
||||
@@ -86,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) {
|
||||
@@ -132,33 +201,60 @@ public class EventService {
|
||||
* Supprime un événement par son ID.
|
||||
*
|
||||
* @param id L'ID de l'événement à supprimer.
|
||||
* @param userId L'ID de l'utilisateur qui tente de supprimer l'événement.
|
||||
* @return true si l'événement a été supprimé, false sinon.
|
||||
* @throws EventNotFoundException Si l'événement n'est pas trouvé.
|
||||
* @throws SecurityException Si l'utilisateur n'est pas le créateur de l'événement.
|
||||
*/
|
||||
@Transactional
|
||||
public boolean deleteEvent(UUID id) {
|
||||
logger.info("[logger] Tentative de suppression de l'événement avec l'ID : {}", id);
|
||||
public boolean deleteEvent(UUID id, UUID userId) {
|
||||
logger.info("[logger] Tentative de suppression de l'événement avec l'ID : {} par l'utilisateur : {}", id, userId);
|
||||
|
||||
Events event = eventsRepository.findById(id);
|
||||
if (event == null) {
|
||||
logger.warn("[logger] Échec de la suppression : événement avec l'ID {} introuvable.", id);
|
||||
throw new EventNotFoundException(id);
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est le créateur
|
||||
if (!canModifyEvent(event, userId)) {
|
||||
logger.error("[ERROR] L'utilisateur {} n'a pas les permissions pour supprimer l'événement {}", userId, id);
|
||||
throw new SecurityException("Vous n'avez pas les permissions pour supprimer cet événement");
|
||||
}
|
||||
|
||||
boolean deleted = eventsRepository.deleteById(id);
|
||||
if (deleted) {
|
||||
logger.info("[logger] Événement avec l'ID {} supprimé avec succès.", id);
|
||||
} else {
|
||||
logger.warn("[logger] Échec de la suppression : événement avec l'ID {} introuvable.", id);
|
||||
throw new EventNotFoundException(id);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un utilisateur peut modifier un événement.
|
||||
*
|
||||
* @param event L'événement à vérifier.
|
||||
* @param userId L'ID de l'utilisateur.
|
||||
* @return true si l'utilisateur peut modifier l'événement, false sinon.
|
||||
*/
|
||||
public boolean canModifyEvent(Events event, UUID userId) {
|
||||
if (event == null || event.getCreator() == null) {
|
||||
return false;
|
||||
}
|
||||
return event.getCreator().getId().equals(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un événement dans le système.
|
||||
*
|
||||
* @param event L'événement contenant les détails mis à jour.
|
||||
* @param userId L'ID de l'utilisateur qui tente de mettre à jour l'événement.
|
||||
* @return L'événement mis à jour.
|
||||
* @throws EventNotFoundException Si l'événement n'est pas trouvé.
|
||||
* @throws SecurityException Si l'utilisateur n'est pas le créateur de l'événement.
|
||||
*/
|
||||
@Transactional
|
||||
public Events updateEvent(Events event) {
|
||||
logger.info("[logger] Tentative de mise à jour de l'événement avec l'ID : {}", event.getId());
|
||||
public Events updateEvent(Events event, UUID userId) {
|
||||
logger.info("[logger] Tentative de mise à jour de l'événement avec l'ID : {} par l'utilisateur : {}", event.getId(), userId);
|
||||
|
||||
Events existingEvent = eventsRepository.findById(event.getId());
|
||||
if (existingEvent == null) {
|
||||
@@ -166,15 +262,45 @@ public class EventService {
|
||||
throw new EventNotFoundException(event.getId());
|
||||
}
|
||||
|
||||
// Mettre à jour les détails de l'événement
|
||||
// Vérifier que l'utilisateur est le créateur
|
||||
if (!canModifyEvent(existingEvent, userId)) {
|
||||
logger.error("[ERROR] L'utilisateur {} n'a pas les permissions pour modifier l'événement {}", userId, event.getId());
|
||||
throw new SecurityException("Vous n'avez pas les permissions pour modifier cet é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());
|
||||
existingEvent.setMaxParticipants(event.getMaxParticipants());
|
||||
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());
|
||||
existingEvent.setAccessibilityInfo(event.getAccessibilityInfo());
|
||||
existingEvent.setParkingInfo(event.getParkingInfo());
|
||||
existingEvent.setSecurityProtocol(event.getSecurityProtocol());
|
||||
existingEvent.setStatus(event.getStatus());
|
||||
|
||||
// Persiste les modifications dans la base de données
|
||||
@@ -216,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;
|
||||
}
|
||||
@@ -319,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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.dto.events.PresenceEvent;
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import com.lions.dev.websocket.NotificationWebSocket;
|
||||
import io.quarkus.logging.Log;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.context.control.ActivateRequestContext;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.eclipse.microprofile.reactive.messaging.Channel;
|
||||
import org.eclipse.microprofile.reactive.messaging.Emitter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -16,7 +21,10 @@ import java.util.*;
|
||||
* Ce service gère:
|
||||
* - Le marquage des utilisateurs comme en ligne/hors ligne
|
||||
* - Le heartbeat pour maintenir le statut online
|
||||
* - La diffusion de la présence aux amis via WebSocket
|
||||
* - La diffusion de la présence aux amis via Kafka → WebSocket
|
||||
*
|
||||
* Architecture v2.0:
|
||||
* PresenceService → Kafka Topic (presence.updates) → PresenceKafkaBridge → WebSocket → Client
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class PresenceService {
|
||||
@@ -24,6 +32,10 @@ public class PresenceService {
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
@Inject
|
||||
@Channel("presence")
|
||||
Emitter<PresenceEvent> presenceEmitter; // v2.0 - Publie dans Kafka
|
||||
|
||||
/**
|
||||
* Marque un utilisateur comme en ligne et broadcast sa présence.
|
||||
*
|
||||
@@ -64,9 +76,11 @@ public class PresenceService {
|
||||
|
||||
/**
|
||||
* Met à jour le heartbeat d'un utilisateur (keep-alive).
|
||||
* Cette méthode est appelée depuis un thread worker, donc elle doit activer le contexte de requête.
|
||||
*
|
||||
* @param userId L'ID de l'utilisateur
|
||||
*/
|
||||
@ActivateRequestContext
|
||||
@Transactional
|
||||
public void heartbeat(UUID userId) {
|
||||
Users user = usersRepository.findById(userId);
|
||||
@@ -78,7 +92,8 @@ public class PresenceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast la présence d'un utilisateur à tous les utilisateurs connectés via WebSocket.
|
||||
* Broadcast la présence d'un utilisateur via Kafka (v2.0).
|
||||
* Le PresenceKafkaBridge consommera depuis Kafka et enverra via WebSocket.
|
||||
*
|
||||
* @param userId L'ID de l'utilisateur
|
||||
* @param isOnline Le statut online
|
||||
@@ -86,18 +101,25 @@ public class PresenceService {
|
||||
*/
|
||||
private void broadcastPresenceToAll(UUID userId, boolean isOnline, LocalDateTime lastSeen) {
|
||||
try {
|
||||
Map<String, Object> presenceData = new HashMap<>();
|
||||
presenceData.put("userId", userId.toString());
|
||||
presenceData.put("isOnline", isOnline);
|
||||
presenceData.put("lastSeen", lastSeen != null ? lastSeen.toString() : null);
|
||||
presenceData.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
// Envoyer via NotificationWebSocket
|
||||
NotificationWebSocket.broadcastPresenceUpdate(presenceData);
|
||||
|
||||
System.out.println("[PRESENCE] Broadcast de la présence de " + userId + " : " + isOnline);
|
||||
// Convertir LocalDateTime en timestamp (milliseconds)
|
||||
Long lastSeenTimestamp = lastSeen != null
|
||||
? lastSeen.toInstant(ZoneOffset.UTC).toEpochMilli()
|
||||
: null;
|
||||
|
||||
// Créer l'événement de présence
|
||||
PresenceEvent presenceEvent = new PresenceEvent(
|
||||
userId.toString(),
|
||||
isOnline ? "online" : "offline",
|
||||
lastSeenTimestamp
|
||||
);
|
||||
|
||||
// Publier dans Kafka (le bridge s'occupera de l'envoi WebSocket)
|
||||
presenceEmitter.send(presenceEvent);
|
||||
|
||||
Log.debug("[PRESENCE] Événement de présence publié dans Kafka: " + userId + " -> " +
|
||||
(isOnline ? "online" : "offline"));
|
||||
} catch (Exception e) {
|
||||
System.out.println("[ERROR] Erreur lors du broadcast de présence : " + e.getMessage());
|
||||
Log.error("[PRESENCE] Erreur lors de la publication de présence dans Kafka", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,16 +36,28 @@ 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(
|
||||
userCreateRequestDTO.getProfileImageUrl() != null
|
||||
? userCreateRequestDTO.getProfileImageUrl()
|
||||
: "https://via.placeholder.com/150"
|
||||
: "https://placehold.co/150x150.png"
|
||||
);
|
||||
user.setRole(
|
||||
userCreateRequestDTO.getRole() != null
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
301
src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java
Normal file
301
src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -1,365 +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).
|
||||
*/
|
||||
private void handlePing(String userId) {
|
||||
try {
|
||||
UUID userUUID = UUID.fromString(userId);
|
||||
|
||||
// Mettre à jour le heartbeat de présence
|
||||
presenceService.heartbeat(userUUID);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param message Message Kafka contenant un ChatMessageEvent
|
||||
* @return CompletionStage pour gérer l'ack/nack asynchrone
|
||||
*/
|
||||
@Incoming("kafka-chat")
|
||||
public CompletionStage<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);
|
||||
|
||||
Log.debug("[CHAT-BRIDGE] Message routé vers WebSocket pour: " + event.getRecipientId());
|
||||
|
||||
// Acknowledger le message Kafka
|
||||
return message.ack();
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.error("[CHAT-BRIDGE] UUID invalide dans l'événement", e);
|
||||
return message.nack(e);
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-BRIDGE] Erreur traitement événement", e);
|
||||
return 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\"}}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
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;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return CompletionStage pour gérer l'ack/nack asynchrone
|
||||
*/
|
||||
@Incoming("kafka-notifications")
|
||||
public CompletionStage<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);
|
||||
|
||||
Log.debug("[KAFKA-BRIDGE] Notification routée vers WebSocket pour: " + event.getUserId());
|
||||
|
||||
// Acknowledger le message Kafka
|
||||
return message.ack();
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.error("[KAFKA-BRIDGE] UUID invalide dans l'événement", e);
|
||||
return message.nack(e);
|
||||
} catch (Exception e) {
|
||||
Log.error("[KAFKA-BRIDGE] Erreur traitement événement", e);
|
||||
return 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\"}}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.lions.dev.websocket.bridge;
|
||||
|
||||
import com.lions.dev.dto.events.PresenceEvent;
|
||||
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.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Bridge qui consomme depuis Kafka et envoie via WebSocket pour les événements de présence.
|
||||
*
|
||||
* Architecture:
|
||||
* PresenceService → Kafka Topic (presence.updates) → Bridge → WebSocket → Client
|
||||
*
|
||||
* Les événements de présence (online/offline) sont diffusés à tous les utilisateurs connectés
|
||||
* pour mettre à jour leur liste d'amis en temps réel.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class PresenceKafkaBridge {
|
||||
|
||||
/**
|
||||
* Consomme les événements de présence depuis Kafka et les route vers WebSocket.
|
||||
*
|
||||
* @param message Message Kafka contenant un PresenceEvent
|
||||
* @return CompletionStage pour gérer l'ack/nack asynchrone
|
||||
*/
|
||||
@Incoming("kafka-presence")
|
||||
public CompletionStage<Void> processPresence(Message<PresenceEvent> message) {
|
||||
try {
|
||||
PresenceEvent event = message.getPayload();
|
||||
|
||||
Log.debug("[PRESENCE-BRIDGE] Événement de présence reçu: " + event.getStatus() +
|
||||
" pour utilisateur: " + event.getUserId());
|
||||
|
||||
// Broadcast à tous les utilisateurs connectés (pas seulement le propriétaire)
|
||||
// Car la présence doit être visible par tous les amis
|
||||
NotificationWebSocketNext.broadcastPresenceUpdate(buildPresenceData(event));
|
||||
|
||||
Log.debug("[PRESENCE-BRIDGE] Présence routée vers WebSocket pour: " + event.getUserId());
|
||||
|
||||
// Acknowledger le message Kafka
|
||||
return message.ack();
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.error("[PRESENCE-BRIDGE] UUID invalide dans l'événement", e);
|
||||
return message.nack(e);
|
||||
} catch (Exception e) {
|
||||
Log.error("[PRESENCE-BRIDGE] Erreur traitement événement", e);
|
||||
return message.nack(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit les données de présence au format attendu par NotificationWebSocketNext.
|
||||
*/
|
||||
private Map<String, Object> buildPresenceData(PresenceEvent event) {
|
||||
Map<String, Object> presenceData = new HashMap<>();
|
||||
presenceData.put("userId", event.getUserId());
|
||||
presenceData.put("isOnline", "online".equals(event.getStatus()));
|
||||
if (event.getLastSeen() != null) {
|
||||
// Convertir timestamp en ISO string si nécessaire
|
||||
presenceData.put("lastSeen", new java.util.Date(event.getLastSeen()).toString());
|
||||
}
|
||||
presenceData.put("timestamp", event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis());
|
||||
return presenceData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param message Message Kafka contenant un ReactionEvent
|
||||
* @return CompletionStage pour gérer l'ack/nack asynchrone
|
||||
*/
|
||||
@Incoming("kafka-reactions")
|
||||
public CompletionStage<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
|
||||
return message.ack();
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.error("[REACTION-BRIDGE] UUID invalide dans l'événement", e);
|
||||
return message.nack(e);
|
||||
} catch (Exception e) {
|
||||
Log.error("[REACTION-BRIDGE] Erreur traitement événement", e);
|
||||
return 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\"}}";
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/main/resources/application-dev.properties
Normal file
31
src/main/resources/application-dev.properties
Normal file
@@ -0,0 +1,31 @@
|
||||
# ====================================================================
|
||||
# AfterWork Server - Configuration DÉVELOPPEMENT
|
||||
# ====================================================================
|
||||
# Ce fichier est automatiquement chargé avec: mvn quarkus:dev
|
||||
# Les configurations ici surchargent celles de application.properties
|
||||
|
||||
# ====================================================================
|
||||
# Base de données H2 (en mémoire)
|
||||
# ====================================================================
|
||||
quarkus.datasource.db-kind=h2
|
||||
quarkus.datasource.jdbc.url=jdbc:h2:mem:afterwork_db;DB_CLOSE_DELAY=-1
|
||||
quarkus.datasource.username=sa
|
||||
quarkus.datasource.password=
|
||||
quarkus.datasource.jdbc.driver=org.h2.Driver
|
||||
quarkus.datasource.devservices.enabled=false
|
||||
|
||||
# ====================================================================
|
||||
# Hibernate ORM
|
||||
# ====================================================================
|
||||
quarkus.hibernate-orm.database.generation=drop-and-create
|
||||
quarkus.hibernate-orm.log.sql=true
|
||||
quarkus.hibernate-orm.format_sql=true
|
||||
quarkus.hibernate-orm.packages=com.lions.dev.entity
|
||||
# Forcer la création du schéma au démarrage
|
||||
quarkus.hibernate-orm.schema-generation.scripts.action=drop-and-create
|
||||
|
||||
# ====================================================================
|
||||
# Logging
|
||||
# ====================================================================
|
||||
quarkus.log.level=DEBUG
|
||||
quarkus.log.category."com.lions.dev".level=DEBUG
|
||||
@@ -1,70 +0,0 @@
|
||||
# ====================================================================
|
||||
# AfterWork Server - Configuration de Production
|
||||
# ====================================================================
|
||||
# IMPORTANT: Les propriétés build-time (app.name, root-path, compression)
|
||||
# sont définies dans application.properties et ne peuvent pas être changées ici
|
||||
|
||||
# HTTP Configuration (runtime only)
|
||||
quarkus.http.host=0.0.0.0
|
||||
quarkus.http.port=8080
|
||||
|
||||
# Base de données PostgreSQL (Production)
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:postgresql}:${DB_PORT:5432}/${DB_NAME:mic-after-work-server-impl-quarkus-main}
|
||||
quarkus.datasource.username=${DB_USERNAME:lionsuser}
|
||||
quarkus.datasource.password=${DB_PASSWORD:LionsUser2025!}
|
||||
quarkus.datasource.jdbc.driver=org.postgresql.Driver
|
||||
quarkus.datasource.jdbc.max-size=20
|
||||
quarkus.datasource.jdbc.min-size=5
|
||||
|
||||
# Hibernate
|
||||
quarkus.hibernate-orm.database.generation=update
|
||||
quarkus.hibernate-orm.log.sql=false
|
||||
quarkus.hibernate-orm.sql-load-script=no-file
|
||||
quarkus.hibernate-orm.jdbc.statement-batch-size=20
|
||||
|
||||
# CORS - Production strict
|
||||
quarkus.http.cors=true
|
||||
quarkus.http.cors.origins=https://afterwork.lions.dev
|
||||
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS,PATCH
|
||||
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
|
||||
quarkus.http.cors.exposed-headers=content-disposition
|
||||
quarkus.http.cors.access-control-max-age=24H
|
||||
quarkus.http.cors.access-control-allow-credentials=true
|
||||
|
||||
# Logging
|
||||
quarkus.log.level=INFO
|
||||
quarkus.log.console.enable=true
|
||||
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n
|
||||
quarkus.log.console.json=false
|
||||
quarkus.log.category."com.lions.dev".level=INFO
|
||||
quarkus.log.category."org.hibernate".level=WARN
|
||||
quarkus.log.category."io.quarkus".level=INFO
|
||||
|
||||
# OpenAPI/Swagger - Configuration build-time dans application.properties
|
||||
|
||||
# Health checks - Utilise les valeurs par défaut de Quarkus
|
||||
|
||||
# Métriques - Configuration build-time dans application.properties
|
||||
|
||||
# WebSocket
|
||||
quarkus.websocket.max-frame-size=65536
|
||||
|
||||
# Upload de fichiers
|
||||
quarkus.http.body.uploads-directory=/tmp/uploads
|
||||
|
||||
# Compression HTTP - Configuration build-time dans application.properties
|
||||
|
||||
# SSL/TLS (géré par le reverse proxy)
|
||||
quarkus.http.ssl.certificate.files=
|
||||
quarkus.http.ssl.certificate.key-files=
|
||||
quarkus.http.insecure-requests=enabled
|
||||
|
||||
# Performance
|
||||
quarkus.thread-pool.core-threads=2
|
||||
quarkus.thread-pool.max-threads=16
|
||||
quarkus.thread-pool.queue-size=100
|
||||
|
||||
# Timezone
|
||||
quarkus.locales=fr-FR,en-US
|
||||
quarkus.default-locale=fr-FR
|
||||
91
src/main/resources/application-production.properties
Normal file
91
src/main/resources/application-production.properties
Normal file
@@ -0,0 +1,91 @@
|
||||
# ====================================================================
|
||||
# AfterWork Server - Configuration PRODUCTION
|
||||
# ====================================================================
|
||||
# Ce fichier est automatiquement chargé avec: java -jar app.jar
|
||||
# Les configurations ici surchargent celles de application.properties
|
||||
|
||||
# ====================================================================
|
||||
# HTTP - Chemin de base de l'API
|
||||
# ====================================================================
|
||||
# Permet d'accéder à l'API via https://api.lions.dev/afterwork
|
||||
quarkus.http.root-path=/afterwork
|
||||
|
||||
# ====================================================================
|
||||
# Swagger/OpenAPI (Production)
|
||||
# ====================================================================
|
||||
# Configuration pour que Swagger UI fonctionne avec root-path
|
||||
quarkus.swagger-ui.enable=true
|
||||
quarkus.swagger-ui.always-include=true
|
||||
quarkus.swagger-ui.path=/q/swagger-ui
|
||||
# Configuration explicite de l'URL OpenAPI pour Swagger UI
|
||||
# Avec root-path=/afterwork, l'OpenAPI spec est à /afterwork/openapi
|
||||
quarkus.swagger-ui.urls.default=/afterwork/openapi
|
||||
quarkus.smallrye-openapi.path=/openapi
|
||||
|
||||
# ====================================================================
|
||||
# Base de données PostgreSQL
|
||||
# ====================================================================
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:postgresql}:${DB_PORT:5432}/${DB_NAME:mic-after-work-server-impl-quarkus-main}
|
||||
quarkus.datasource.username=${DB_USERNAME:lionsuser}
|
||||
quarkus.datasource.password=${DB_PASSWORD:LionsUser2025!}
|
||||
quarkus.datasource.jdbc.driver=org.postgresql.Driver
|
||||
quarkus.datasource.jdbc.max-size=20
|
||||
quarkus.datasource.jdbc.min-size=5
|
||||
quarkus.datasource.devservices.enabled=false
|
||||
|
||||
# ====================================================================
|
||||
# Hibernate ORM
|
||||
# ====================================================================
|
||||
quarkus.hibernate-orm.database.generation=update
|
||||
quarkus.hibernate-orm.log.sql=false
|
||||
quarkus.hibernate-orm.sql-load-script=no-file
|
||||
quarkus.hibernate-orm.jdbc.statement-batch-size=20
|
||||
|
||||
# ====================================================================
|
||||
# CORS (Cross-Origin Resource Sharing)
|
||||
# ====================================================================
|
||||
quarkus.http.cors=true
|
||||
quarkus.http.cors.origins=https://afterwork.lions.dev
|
||||
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS,PATCH
|
||||
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
|
||||
quarkus.http.cors.exposed-headers=content-disposition
|
||||
quarkus.http.cors.access-control-max-age=24H
|
||||
quarkus.http.cors.access-control-allow-credentials=true
|
||||
|
||||
# ====================================================================
|
||||
# Logging
|
||||
# ====================================================================
|
||||
quarkus.log.level=INFO
|
||||
quarkus.log.console.enable=true
|
||||
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n
|
||||
quarkus.log.console.json=false
|
||||
quarkus.log.category."com.lions.dev".level=INFO
|
||||
quarkus.log.category."org.hibernate".level=WARN
|
||||
quarkus.log.category."io.quarkus".level=INFO
|
||||
|
||||
# ====================================================================
|
||||
# WebSocket
|
||||
# ====================================================================
|
||||
# Note: La propriété quarkus.websocket.max-frame-size n'existe pas dans Quarkus 3.16
|
||||
# Les WebSockets Next utilisent une configuration différente si nécessaire
|
||||
|
||||
# ====================================================================
|
||||
# SSL/TLS (géré par le reverse proxy)
|
||||
# ====================================================================
|
||||
quarkus.http.ssl.certificate.files=
|
||||
quarkus.http.ssl.certificate.key-files=
|
||||
quarkus.http.insecure-requests=enabled
|
||||
|
||||
# ====================================================================
|
||||
# Performance
|
||||
# ====================================================================
|
||||
quarkus.thread-pool.core-threads=2
|
||||
quarkus.thread-pool.max-threads=16
|
||||
quarkus.thread-pool.queue-size=100
|
||||
|
||||
# ====================================================================
|
||||
# Localisation
|
||||
# ====================================================================
|
||||
quarkus.locales=fr-FR,en-US
|
||||
quarkus.default-locale=fr-FR
|
||||
@@ -1,63 +1,125 @@
|
||||
# Configuration Swagger UI
|
||||
# ====================================================================
|
||||
# AfterWork Server - Configuration Commune
|
||||
# ====================================================================
|
||||
# Ce fichier contient les configurations partagées par tous les environnements.
|
||||
# Les configurations spécifiques sont dans :
|
||||
# - application-dev.properties (développement)
|
||||
# - application-prod.properties (production)
|
||||
# - application-production.properties (production - profil "production")
|
||||
#
|
||||
# NOTE: Configuration datasource par défaut pour les tests
|
||||
# Les profils dev/prod/production surchargent cette configuration
|
||||
|
||||
# ====================================================================
|
||||
# Swagger/OpenAPI (commun à tous les environnements)
|
||||
# ====================================================================
|
||||
quarkus.swagger-ui.always-include=true
|
||||
quarkus.swagger-ui.path=/q/swagger-ui
|
||||
quarkus.smallrye-openapi.path=/openapi
|
||||
|
||||
# Configuration datasource par défaut (PostgreSQL) pour le build
|
||||
# Les valeurs seront remplacées au runtime par les variables d'environnement
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.datasource.jdbc.url=jdbc:postgresql://postgresql:5432/mic-after-work-server-impl-quarkus-main
|
||||
quarkus.datasource.username=lionsuser
|
||||
quarkus.datasource.password=LionsUser2025!
|
||||
quarkus.datasource.jdbc.driver=org.postgresql.Driver
|
||||
quarkus.hibernate-orm.database.generation=update
|
||||
# ====================================================================
|
||||
# HTTP (commun à tous les environnements)
|
||||
# ====================================================================
|
||||
quarkus.http.host=0.0.0.0
|
||||
quarkus.http.port=8080
|
||||
|
||||
# ====================================================================
|
||||
# Base de données (configuration par défaut pour les tests)
|
||||
# ====================================================================
|
||||
# Cette configuration est utilisée par défaut si aucun profil n'est spécifié
|
||||
# Les profils dev/prod/production surchargent cette configuration
|
||||
quarkus.datasource.db-kind=h2
|
||||
quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
|
||||
quarkus.datasource.username=sa
|
||||
quarkus.datasource.password=
|
||||
quarkus.datasource.jdbc.driver=org.h2.Driver
|
||||
quarkus.datasource.devservices.enabled=false
|
||||
|
||||
# Hibernate ORM (par défaut)
|
||||
quarkus.hibernate-orm.database.generation=drop-and-create
|
||||
quarkus.hibernate-orm.log.sql=false
|
||||
|
||||
# Configuration de la base de données H2 (en mémoire) pour Quarkus en développement
|
||||
%dev.quarkus.datasource.db-kind=h2
|
||||
%dev.quarkus.datasource.jdbc.url=jdbc:h2:mem:afterwork_db;DB_CLOSE_DELAY=-1
|
||||
%dev.quarkus.datasource.username=sa
|
||||
%dev.quarkus.datasource.password=
|
||||
%dev.quarkus.hibernate-orm.database.generation=drop-and-create
|
||||
%dev.quarkus.hibernate-orm.log.sql=true
|
||||
%dev.quarkus.datasource.devservices.enabled=false
|
||||
|
||||
# Configuration PostgreSQL (production) - commentée pour les tests
|
||||
# %dev.quarkus.datasource.db-kind=postgresql
|
||||
# %dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/afterwork_db
|
||||
# %dev.quarkus.datasource.username=afterwork
|
||||
# %dev.quarkus.datasource.password=@ft3rw0rk
|
||||
# %dev.quarkus.datasource.jdbc.driver=org.postgresql.Driver
|
||||
|
||||
# Configuration de la base de données PostgreSQL pour Quarkus en production
|
||||
%prod.quarkus.datasource.db-kind=postgresql
|
||||
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:postgresql}:${DB_PORT:5432}/${DB_NAME:mic-after-work-server-impl-quarkus-main}
|
||||
%prod.quarkus.datasource.username=${DB_USERNAME:lionsuser}
|
||||
%prod.quarkus.datasource.password=${DB_PASSWORD:LionsUser2025!}
|
||||
%prod.quarkus.datasource.jdbc.driver=org.postgresql.Driver
|
||||
%prod.quarkus.hibernate-orm.database.generation=update
|
||||
%prod.quarkus.hibernate-orm.log.sql=false
|
||||
%prod.quarkus.datasource.devservices.enabled=false
|
||||
|
||||
# Niveau de logging pour Quarkus en développement
|
||||
%dev.quarkus.log.level=DEBUG
|
||||
|
||||
# Niveau de logging pour Quarkus en production
|
||||
%prod.quarkus.log.level=INFO
|
||||
|
||||
# Configuration de la signature JWT (désactivée pour l'instant)
|
||||
# mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem
|
||||
# mp.jwt.verify.issuer=https://issuer.example.com
|
||||
# mp.jwt.token.header=Authorization
|
||||
# mp.jwt.token.schemes=Bearer
|
||||
# smallrye.jwt.sign.key.location=META-INF/resources/privateKey.pem
|
||||
# smallrye.jwt.sign.key.algorithm=RS256
|
||||
# smallrye.jwt.token.lifetime=3600
|
||||
|
||||
# Activer le support multipart pour l'upload de fichiers
|
||||
# ====================================================================
|
||||
# Upload de fichiers (commun à tous les environnements)
|
||||
# ====================================================================
|
||||
quarkus.http.body.uploads-directory=/tmp/uploads
|
||||
quarkus.http.limits.max-body-size=10M
|
||||
|
||||
# Écouter sur toutes les interfaces réseau (0.0.0.0) pour être accessible depuis le Samsung
|
||||
quarkus.http.host=0.0.0.0
|
||||
quarkus.http.port=8080
|
||||
# ====================================================================
|
||||
# WebSockets Next (commun à tous les environnements)
|
||||
# ====================================================================
|
||||
# Note: WebSockets Next est activé par défaut si la dépendance est présente
|
||||
# La propriété quarkus.websockets-next.server.enabled n'existe pas dans Quarkus 3.16
|
||||
# WebSockets Next est automatiquement activé quand quarkus-websockets-next est dans les dépendances
|
||||
|
||||
# ====================================================================
|
||||
# Kafka Configuration (commun à tous les environnements)
|
||||
# ====================================================================
|
||||
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
|
||||
|
||||
# ====================================================================
|
||||
# Kafka Topics - Outgoing (Services → Kafka)
|
||||
# ====================================================================
|
||||
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<EventType>
|
||||
# Topic: Notifications
|
||||
mp.messaging.outgoing.notifications.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.notifications.topic=notifications
|
||||
mp.messaging.outgoing.notifications.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<NotificationEvent>
|
||||
|
||||
# Topic: Chat Messages
|
||||
mp.messaging.outgoing.chat-messages.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.chat-messages.topic=chat.messages
|
||||
mp.messaging.outgoing.chat-messages.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<ChatMessageEvent>
|
||||
|
||||
# Topic: Reactions (likes, comments, shares)
|
||||
mp.messaging.outgoing.reactions.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.reactions.topic=reactions
|
||||
mp.messaging.outgoing.reactions.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<ReactionEvent>
|
||||
|
||||
# Topic: Presence Updates
|
||||
mp.messaging.outgoing.presence.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.presence.topic=presence.updates
|
||||
mp.messaging.outgoing.presence.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<PresenceEvent>
|
||||
|
||||
# ====================================================================
|
||||
# Kafka Topics - Incoming (Kafka → WebSocket Bridge)
|
||||
# ====================================================================
|
||||
# Consommer depuis Kafka et router vers WebSocket pour notifications
|
||||
# Note: Quarkus génère automatiquement les deserializers Jackson basés sur le type générique Message<NotificationEvent>
|
||||
mp.messaging.incoming.kafka-notifications.connector=smallrye-kafka
|
||||
mp.messaging.incoming.kafka-notifications.topic=notifications
|
||||
mp.messaging.incoming.kafka-notifications.group.id=websocket-notifications-bridge
|
||||
mp.messaging.incoming.kafka-notifications.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
# value.deserializer omis - Quarkus génère automatiquement depuis Message<NotificationEvent>
|
||||
mp.messaging.incoming.kafka-notifications.enable.auto.commit=true
|
||||
|
||||
# Consommer depuis Kafka et router vers WebSocket pour chat
|
||||
# Note: Quarkus génère automatiquement les deserializers Jackson basés sur le type générique Message<ChatMessageEvent>
|
||||
mp.messaging.incoming.kafka-chat.connector=smallrye-kafka
|
||||
mp.messaging.incoming.kafka-chat.topic=chat.messages
|
||||
mp.messaging.incoming.kafka-chat.group.id=websocket-chat-bridge
|
||||
mp.messaging.incoming.kafka-chat.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
# value.deserializer omis - Quarkus génère automatiquement depuis Message<ChatMessageEvent>
|
||||
mp.messaging.incoming.kafka-chat.enable.auto.commit=true
|
||||
|
||||
# Consommer depuis Kafka et router vers WebSocket pour réactions
|
||||
# Note: Quarkus génère automatiquement les deserializers Jackson basés sur le type générique Message<ReactionEvent>
|
||||
mp.messaging.incoming.kafka-reactions.connector=smallrye-kafka
|
||||
mp.messaging.incoming.kafka-reactions.topic=reactions
|
||||
mp.messaging.incoming.kafka-reactions.group.id=websocket-reactions-bridge
|
||||
mp.messaging.incoming.kafka-reactions.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
# value.deserializer omis - Quarkus génère automatiquement depuis Message<ReactionEvent>
|
||||
mp.messaging.incoming.kafka-reactions.enable.auto.commit=true
|
||||
|
||||
# Consommer depuis Kafka et router vers WebSocket pour présence
|
||||
# Note: Quarkus génère automatiquement les deserializers Jackson basés sur le type générique Message<PresenceEvent>
|
||||
mp.messaging.incoming.kafka-presence.connector=smallrye-kafka
|
||||
mp.messaging.incoming.kafka-presence.topic=presence.updates
|
||||
mp.messaging.incoming.kafka-presence.group.id=websocket-presence-bridge
|
||||
mp.messaging.incoming.kafka-presence.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
# value.deserializer omis - Quarkus génère automatiquement depuis Message<PresenceEvent>
|
||||
mp.messaging.incoming.kafka-presence.enable.auto.commit=true
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
-- Migration V10: Création de la table bookings pour remplacer/étendre reservations
|
||||
-- Date: 2026-01-15
|
||||
-- Description: Nouvelle table pour les réservations avec plus de détails (v2.0)
|
||||
|
||||
-- Note: Cette migration crée une nouvelle table "bookings" qui remplace/étend "reservations"
|
||||
-- La table reservations peut être conservée pour compatibilité ou migrée progressivement
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bookings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
establishment_id UUID NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
reservation_time TIMESTAMP NOT NULL, -- Date et heure de la réservation
|
||||
guest_count INTEGER NOT NULL DEFAULT 1, -- Nombre de convives
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING, CONFIRMED, CANCELLED, COMPLETED
|
||||
special_requests TEXT, -- Demandes spéciales (allergies, préférences, etc.)
|
||||
table_number VARCHAR(20), -- Numéro de table assigné (si applicable)
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
CONSTRAINT fk_bookings_establishment
|
||||
FOREIGN KEY (establishment_id)
|
||||
REFERENCES establishments(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_bookings_user
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES users(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT chk_bookings_guest_count
|
||||
CHECK (guest_count > 0),
|
||||
|
||||
CONSTRAINT chk_bookings_status
|
||||
CHECK (status IN ('PENDING', 'CONFIRMED', 'CANCELLED', 'COMPLETED'))
|
||||
);
|
||||
|
||||
-- Créer les index pour améliorer les performances
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_establishment
|
||||
ON bookings(establishment_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_user
|
||||
ON bookings(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_status
|
||||
ON bookings(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_reservation_time
|
||||
ON bookings(reservation_time);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_establishment_time
|
||||
ON bookings(establishment_id, reservation_time);
|
||||
|
||||
-- Index pour les requêtes de réservations à venir
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_upcoming
|
||||
ON bookings(establishment_id, reservation_time)
|
||||
WHERE reservation_time >= CURRENT_TIMESTAMP AND status IN ('PENDING', 'CONFIRMED');
|
||||
|
||||
-- Créer un trigger pour mettre à jour updated_at automatiquement
|
||||
CREATE OR REPLACE FUNCTION update_bookings_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_bookings_updated_at
|
||||
BEFORE UPDATE ON bookings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_bookings_updated_at();
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON TABLE bookings IS 'Réservations d''établissements (v2.0 - remplace/étend reservations)';
|
||||
COMMENT ON COLUMN bookings.establishment_id IS 'Établissement concerné par la réservation';
|
||||
COMMENT ON COLUMN bookings.user_id IS 'Utilisateur qui a fait la réservation';
|
||||
COMMENT ON COLUMN bookings.reservation_time IS 'Date et heure de la réservation';
|
||||
COMMENT ON COLUMN bookings.guest_count IS 'Nombre de convives';
|
||||
COMMENT ON COLUMN bookings.status IS 'Statut: PENDING, CONFIRMED, CANCELLED, COMPLETED';
|
||||
COMMENT ON COLUMN bookings.special_requests IS 'Demandes spéciales (allergies, préférences, etc.)';
|
||||
COMMENT ON COLUMN bookings.table_number IS 'Numéro de table assigné (si applicable)';
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
-- Migration V11: Création de la table promotions pour les offres spéciales
|
||||
-- Date: 2026-01-15
|
||||
-- Description: Table pour gérer les promotions et happy hours des établissements (v2.0)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS promotions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
establishment_id UUID NOT NULL,
|
||||
title VARCHAR(200) NOT NULL, -- Titre de la promotion (ex: "Happy Hour", "Menu du jour")
|
||||
description TEXT, -- Description détaillée de la promotion
|
||||
promo_code VARCHAR(50) UNIQUE, -- Code promo optionnel (ex: "HAPPY2024")
|
||||
discount_type VARCHAR(20) NOT NULL, -- PERCENTAGE, FIXED_AMOUNT, FREE_ITEM
|
||||
discount_value NUMERIC(10,2) NOT NULL, -- Valeur de la réduction (pourcentage, montant fixe, etc.)
|
||||
valid_from TIMESTAMP NOT NULL, -- Date de début de validité
|
||||
valid_until TIMESTAMP NOT NULL, -- Date de fin de validité
|
||||
is_active BOOLEAN DEFAULT true NOT NULL, -- Indique si la promotion est active
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
CONSTRAINT fk_promotions_establishment
|
||||
FOREIGN KEY (establishment_id)
|
||||
REFERENCES establishments(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT chk_promotions_discount_type
|
||||
CHECK (discount_type IN ('PERCENTAGE', 'FIXED_AMOUNT', 'FREE_ITEM')),
|
||||
|
||||
CONSTRAINT chk_promotions_discount_value
|
||||
CHECK (discount_value >= 0),
|
||||
|
||||
CONSTRAINT chk_promotions_valid_dates
|
||||
CHECK (valid_until > valid_from)
|
||||
);
|
||||
|
||||
-- Créer les index pour améliorer les performances
|
||||
CREATE INDEX IF NOT EXISTS idx_promotions_establishment
|
||||
ON promotions(establishment_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_promotions_active
|
||||
ON promotions(establishment_id, is_active)
|
||||
WHERE is_active = true;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_promotions_valid_dates
|
||||
ON promotions(establishment_id, valid_from, valid_until);
|
||||
|
||||
-- Index pour les promotions actives et valides
|
||||
CREATE INDEX IF NOT EXISTS idx_promotions_active_valid
|
||||
ON promotions(establishment_id, valid_from, valid_until)
|
||||
WHERE is_active = true AND valid_from <= CURRENT_TIMESTAMP AND valid_until >= CURRENT_TIMESTAMP;
|
||||
|
||||
-- Index pour les recherches par code promo
|
||||
CREATE INDEX IF NOT EXISTS idx_promotions_promo_code
|
||||
ON promotions(promo_code)
|
||||
WHERE promo_code IS NOT NULL;
|
||||
|
||||
-- Créer un trigger pour mettre à jour updated_at automatiquement
|
||||
CREATE OR REPLACE FUNCTION update_promotions_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_promotions_updated_at
|
||||
BEFORE UPDATE ON promotions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_promotions_updated_at();
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON TABLE promotions IS 'Promotions et offres spéciales des établissements (v2.0)';
|
||||
COMMENT ON COLUMN promotions.establishment_id IS 'Établissement qui propose la promotion';
|
||||
COMMENT ON COLUMN promotions.title IS 'Titre de la promotion';
|
||||
COMMENT ON COLUMN promotions.description IS 'Description détaillée de la promotion';
|
||||
COMMENT ON COLUMN promotions.promo_code IS 'Code promo optionnel pour activer la promotion';
|
||||
COMMENT ON COLUMN promotions.discount_type IS 'Type de réduction: PERCENTAGE, FIXED_AMOUNT, FREE_ITEM';
|
||||
COMMENT ON COLUMN promotions.discount_value IS 'Valeur de la réduction (pourcentage, montant fixe, etc.)';
|
||||
COMMENT ON COLUMN promotions.valid_from IS 'Date de début de validité';
|
||||
COMMENT ON COLUMN promotions.valid_until IS 'Date de fin de validité';
|
||||
COMMENT ON COLUMN promotions.is_active IS 'Indique si la promotion est active';
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
-- Migration pour ajouter les champs supplémentaires à la table events
|
||||
-- Date: 2026-01-12
|
||||
-- Description: Ajout des champs manquants pour la création complète d'événements
|
||||
|
||||
-- Ajouter la colonne max_participants
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS max_participants INTEGER;
|
||||
|
||||
-- Ajouter la colonne tags
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS tags VARCHAR(500);
|
||||
|
||||
-- Ajouter la colonne organizer
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS organizer VARCHAR(200);
|
||||
|
||||
-- Ajouter la colonne participation_fee
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS participation_fee INTEGER;
|
||||
|
||||
-- Ajouter la colonne privacy_rules
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS privacy_rules VARCHAR(1000);
|
||||
|
||||
-- Ajouter la colonne transport_info
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS transport_info VARCHAR(1000);
|
||||
|
||||
-- Ajouter la colonne accommodation_info
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS accommodation_info VARCHAR(1000);
|
||||
|
||||
-- Ajouter la colonne accessibility_info
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS accessibility_info VARCHAR(1000);
|
||||
|
||||
-- Ajouter la colonne parking_info
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS parking_info VARCHAR(1000);
|
||||
|
||||
-- Ajouter la colonne security_protocol
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS security_protocol VARCHAR(1000);
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON COLUMN events.max_participants IS 'Nombre maximum de participants autorisés';
|
||||
COMMENT ON COLUMN events.tags IS 'Tags/mots-clés associés à l''événement (séparés par des virgules)';
|
||||
COMMENT ON COLUMN events.organizer IS 'Nom de l''organisateur de l''événement';
|
||||
COMMENT ON COLUMN events.participation_fee IS 'Frais de participation en centimes';
|
||||
COMMENT ON COLUMN events.privacy_rules IS 'Règles de confidentialité de l''événement';
|
||||
COMMENT ON COLUMN events.transport_info IS 'Informations sur les transports disponibles';
|
||||
COMMENT ON COLUMN events.accommodation_info IS 'Informations sur l''hébergement';
|
||||
COMMENT ON COLUMN events.accessibility_info IS 'Informations sur l''accessibilité';
|
||||
COMMENT ON COLUMN events.parking_info IS 'Informations sur le parking';
|
||||
COMMENT ON COLUMN events.security_protocol IS 'Protocole de sécurité de l''événement';
|
||||
|
||||
48
src/main/resources/db/migration/V3__Migrate_Users_To_V2.sql
Normal file
48
src/main/resources/db/migration/V3__Migrate_Users_To_V2.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
-- Migration V3: Migration de la table users vers l'architecture v2.0
|
||||
-- Date: 2026-01-15
|
||||
-- Description: Renommage des colonnes et ajout des nouveaux champs pour l'entité User
|
||||
|
||||
-- Renommer les colonnes existantes
|
||||
ALTER TABLE users RENAME COLUMN nom TO first_name;
|
||||
ALTER TABLE users RENAME COLUMN prenoms TO last_name;
|
||||
ALTER TABLE users RENAME COLUMN mot_de_passe TO password_hash;
|
||||
|
||||
-- Ajouter les nouvelles colonnes
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS bio VARCHAR(500);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS loyalty_points INTEGER DEFAULT 0 NOT NULL;
|
||||
|
||||
-- Ajouter la colonne preferences en JSONB (PostgreSQL)
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS preferences JSONB DEFAULT '{}'::jsonb NOT NULL;
|
||||
|
||||
-- Migrer les données de preferred_category vers preferences si nécessaire
|
||||
-- Note: Cette migration préserve les données existantes
|
||||
UPDATE users
|
||||
SET preferences = jsonb_build_object('preferred_category', preferred_category)
|
||||
WHERE preferred_category IS NOT NULL
|
||||
AND (preferences IS NULL OR preferences = '{}'::jsonb);
|
||||
|
||||
-- Supprimer la colonne obsolète preferred_category
|
||||
-- Note: On vérifie d'abord si la colonne existe pour éviter les erreurs
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'users'
|
||||
AND column_name = 'preferred_category'
|
||||
) THEN
|
||||
ALTER TABLE users DROP COLUMN preferred_category;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Créer un index sur preferences si nécessaire (pour les requêtes JSONB)
|
||||
CREATE INDEX IF NOT EXISTS idx_users_preferences ON users USING GIN (preferences);
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON COLUMN users.first_name IS 'Prénom de l''utilisateur (v2.0)';
|
||||
COMMENT ON COLUMN users.last_name IS 'Nom de famille de l''utilisateur (v2.0)';
|
||||
COMMENT ON COLUMN users.password_hash IS 'Mot de passe hashé avec BCrypt (v2.0)';
|
||||
COMMENT ON COLUMN users.bio IS 'Biographie courte de l''utilisateur (v2.0)';
|
||||
COMMENT ON COLUMN users.loyalty_points IS 'Points de fidélité accumulés (v2.0)';
|
||||
COMMENT ON COLUMN users.preferences IS 'Préférences utilisateur en JSONB (v2.0)';
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
-- Migration V4: Migration de la table establishments vers l'architecture v2.0
|
||||
-- Date: 2026-01-15
|
||||
-- Description: Renommage des colonnes, ajout de verification_status, suppression des colonnes obsolètes
|
||||
|
||||
-- Renommer la colonne total_ratings_count vers total_reviews_count
|
||||
ALTER TABLE establishments RENAME COLUMN total_ratings_count TO total_reviews_count;
|
||||
|
||||
-- Ajouter la colonne verification_status
|
||||
ALTER TABLE establishments ADD COLUMN IF NOT EXISTS verification_status VARCHAR(20) DEFAULT 'PENDING' NOT NULL;
|
||||
|
||||
-- Supprimer les colonnes obsolètes (avec vérification pour éviter les erreurs)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Supprimer rating (déprécié, utiliser average_rating)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'establishments' AND column_name = 'rating'
|
||||
) THEN
|
||||
ALTER TABLE establishments DROP COLUMN rating;
|
||||
END IF;
|
||||
|
||||
-- Supprimer email (délégué à la table users via manager_id)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'establishments' AND column_name = 'email'
|
||||
) THEN
|
||||
ALTER TABLE establishments DROP COLUMN email;
|
||||
END IF;
|
||||
|
||||
-- Supprimer image_url (utiliser establishment_media à la place)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'establishments' AND column_name = 'image_url'
|
||||
) THEN
|
||||
ALTER TABLE establishments DROP COLUMN image_url;
|
||||
END IF;
|
||||
|
||||
-- Supprimer capacity (non utilisé dans v2.0)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'establishments' AND column_name = 'capacity'
|
||||
) THEN
|
||||
ALTER TABLE establishments DROP COLUMN capacity;
|
||||
END IF;
|
||||
|
||||
-- Supprimer amenities (délégué à la table establishment_amenities)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'establishments' AND column_name = 'amenities'
|
||||
) THEN
|
||||
-- Optionnel: Migrer les données vers establishment_amenities avant suppression
|
||||
-- (à implémenter si nécessaire)
|
||||
ALTER TABLE establishments DROP COLUMN amenities;
|
||||
END IF;
|
||||
|
||||
-- Supprimer opening_hours (délégué à la table business_hours)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'establishments' AND column_name = 'opening_hours'
|
||||
) THEN
|
||||
-- Optionnel: Migrer les données vers business_hours avant suppression
|
||||
-- (à implémenter si nécessaire)
|
||||
ALTER TABLE establishments DROP COLUMN opening_hours;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Créer un index sur verification_status pour les requêtes de filtrage
|
||||
CREATE INDEX IF NOT EXISTS idx_establishments_verification_status
|
||||
ON establishments(verification_status);
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON COLUMN establishments.total_reviews_count IS 'Nombre total d''avis (v2.0 - renommé depuis total_ratings_count)';
|
||||
COMMENT ON COLUMN establishments.verification_status IS 'Statut de vérification: PENDING, VERIFIED, REJECTED (v2.0)';
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
-- Migration V5: Création de la table business_hours pour gérer les horaires d'ouverture
|
||||
-- Date: 2026-01-15
|
||||
-- Description: Table dédiée pour les horaires d'ouverture des établissements (v2.0)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS business_hours (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
establishment_id UUID NOT NULL,
|
||||
day_of_week VARCHAR(20) NOT NULL, -- MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
|
||||
open_time VARCHAR(5) NOT NULL, -- Format HH:MM (ex: "09:00")
|
||||
close_time VARCHAR(5) NOT NULL, -- Format HH:MM (ex: "18:00")
|
||||
is_closed BOOLEAN DEFAULT false NOT NULL,
|
||||
is_exception BOOLEAN DEFAULT false NOT NULL, -- Pour les jours exceptionnels (fermetures temporaires)
|
||||
exception_date TIMESTAMP, -- Date de l'exception (si is_exception = true)
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
CONSTRAINT fk_business_hours_establishment
|
||||
FOREIGN KEY (establishment_id)
|
||||
REFERENCES establishments(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT chk_business_hours_time_format
|
||||
CHECK (open_time ~ '^([0-1][0-9]|2[0-3]):[0-5][0-9]$'
|
||||
AND close_time ~ '^([0-1][0-9]|2[0-3]):[0-5][0-9]$'),
|
||||
|
||||
CONSTRAINT chk_business_hours_day_of_week
|
||||
CHECK (day_of_week IN ('MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'))
|
||||
);
|
||||
|
||||
-- Créer les index pour améliorer les performances
|
||||
CREATE INDEX IF NOT EXISTS idx_business_hours_establishment
|
||||
ON business_hours(establishment_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_business_hours_day_of_week
|
||||
ON business_hours(day_of_week);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_business_hours_exception
|
||||
ON business_hours(establishment_id, exception_date)
|
||||
WHERE is_exception = true;
|
||||
|
||||
-- Créer un trigger pour mettre à jour updated_at automatiquement
|
||||
CREATE OR REPLACE FUNCTION update_business_hours_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_business_hours_updated_at
|
||||
BEFORE UPDATE ON business_hours
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_business_hours_updated_at();
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON TABLE business_hours IS 'Horaires d''ouverture des établissements (v2.0)';
|
||||
COMMENT ON COLUMN business_hours.establishment_id IS 'Référence vers l''établissement';
|
||||
COMMENT ON COLUMN business_hours.day_of_week IS 'Jour de la semaine (MONDAY à SUNDAY)';
|
||||
COMMENT ON COLUMN business_hours.open_time IS 'Heure d''ouverture (format HH:MM)';
|
||||
COMMENT ON COLUMN business_hours.close_time IS 'Heure de fermeture (format HH:MM)';
|
||||
COMMENT ON COLUMN business_hours.is_closed IS 'Indique si l''établissement est fermé ce jour';
|
||||
COMMENT ON COLUMN business_hours.is_exception IS 'Indique si c''est un jour exceptionnel';
|
||||
COMMENT ON COLUMN business_hours.exception_date IS 'Date de l''exception (si is_exception = true)';
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
-- Migration V6: Création de la table establishment_amenities pour gérer les équipements
|
||||
-- Date: 2026-01-15
|
||||
-- Description: Table de liaison pour les équipements des établissements (v2.0)
|
||||
|
||||
-- Créer d'abord une table de référence pour les types d'équipements (optionnel mais recommandé)
|
||||
CREATE TABLE IF NOT EXISTS amenity_types (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL UNIQUE, -- Ex: "WiFi", "Parking", "Terrasse", "Climatisation"
|
||||
category VARCHAR(50), -- Ex: "Comfort", "Accessibility", "Entertainment"
|
||||
icon VARCHAR(50), -- Nom de l'icône à utiliser dans l'UI
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
CONSTRAINT chk_amenity_types_name_not_empty CHECK (LENGTH(TRIM(name)) > 0)
|
||||
);
|
||||
|
||||
-- Créer la table de liaison establishment_amenities
|
||||
CREATE TABLE IF NOT EXISTS establishment_amenities (
|
||||
establishment_id UUID NOT NULL,
|
||||
amenity_id UUID NOT NULL,
|
||||
details VARCHAR(500), -- Détails supplémentaires (ex: "Parking gratuit pour 20 voitures")
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
PRIMARY KEY (establishment_id, amenity_id),
|
||||
|
||||
CONSTRAINT fk_establishment_amenities_establishment
|
||||
FOREIGN KEY (establishment_id)
|
||||
REFERENCES establishments(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_establishment_amenities_amenity
|
||||
FOREIGN KEY (amenity_id)
|
||||
REFERENCES amenity_types(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Créer les index pour améliorer les performances
|
||||
CREATE INDEX IF NOT EXISTS idx_establishment_amenities_establishment
|
||||
ON establishment_amenities(establishment_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_establishment_amenities_amenity
|
||||
ON establishment_amenities(amenity_id);
|
||||
|
||||
-- Insérer quelques types d'équipements courants (optionnel)
|
||||
INSERT INTO amenity_types (name, category, icon) VALUES
|
||||
('WiFi', 'Comfort', 'wifi'),
|
||||
('Parking', 'Accessibility', 'parking'),
|
||||
('Terrasse', 'Comfort', 'terrace'),
|
||||
('Climatisation', 'Comfort', 'ac'),
|
||||
('Chauffage', 'Comfort', 'heating'),
|
||||
('Accessible PMR', 'Accessibility', 'accessible'),
|
||||
('Animaux acceptés', 'Comfort', 'pets'),
|
||||
('Réservation en ligne', 'Service', 'online_booking'),
|
||||
('Service de livraison', 'Service', 'delivery'),
|
||||
('Bar', 'Entertainment', 'bar'),
|
||||
('Musique live', 'Entertainment', 'live_music'),
|
||||
('Écrans TV', 'Entertainment', 'tv')
|
||||
ON CONFLICT (name) DO NOTHING; -- Éviter les doublons
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON TABLE amenity_types IS 'Types d''équipements disponibles pour les établissements (v2.0)';
|
||||
COMMENT ON TABLE establishment_amenities IS 'Liaison entre établissements et équipements (v2.0)';
|
||||
COMMENT ON COLUMN establishment_amenities.establishment_id IS 'Référence vers l''établissement';
|
||||
COMMENT ON COLUMN establishment_amenities.amenity_id IS 'Référence vers le type d''équipement';
|
||||
COMMENT ON COLUMN establishment_amenities.details IS 'Détails supplémentaires sur l''équipement';
|
||||
|
||||
55
src/main/resources/db/migration/V7__Migrate_Events_To_V2.sql
Normal file
55
src/main/resources/db/migration/V7__Migrate_Events_To_V2.sql
Normal file
@@ -0,0 +1,55 @@
|
||||
-- Migration V7: Migration de la table events vers l'architecture v2.0
|
||||
-- Date: 2026-01-15
|
||||
-- Description: Ajout de establishment_id, is_private, waitlist_enabled et suppression de location
|
||||
|
||||
-- Ajouter la colonne establishment_id (FK vers establishments)
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS establishment_id UUID;
|
||||
|
||||
-- Créer la contrainte de clé étrangère pour establishment_id
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'fk_events_establishment'
|
||||
) THEN
|
||||
ALTER TABLE events
|
||||
ADD CONSTRAINT fk_events_establishment
|
||||
FOREIGN KEY (establishment_id)
|
||||
REFERENCES establishments(id)
|
||||
ON DELETE SET NULL; -- Si l'établissement est supprimé, mettre NULL plutôt que supprimer l'événement
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Ajouter la colonne is_private
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS is_private BOOLEAN DEFAULT false NOT NULL;
|
||||
|
||||
-- Ajouter la colonne waitlist_enabled
|
||||
ALTER TABLE events ADD COLUMN IF NOT EXISTS waitlist_enabled BOOLEAN DEFAULT false NOT NULL;
|
||||
|
||||
-- Créer un index sur establishment_id pour améliorer les performances
|
||||
CREATE INDEX IF NOT EXISTS idx_events_establishment
|
||||
ON events(establishment_id);
|
||||
|
||||
-- Créer un index sur is_private pour les requêtes de filtrage
|
||||
CREATE INDEX IF NOT EXISTS idx_events_is_private
|
||||
ON events(is_private);
|
||||
|
||||
-- Supprimer la colonne location (déléguée à Establishment via establishment_id)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Optionnel: Migrer les données de location vers establishment_id si possible
|
||||
-- (nécessite une logique de mapping personnalisée)
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'events' AND column_name = 'location'
|
||||
) THEN
|
||||
ALTER TABLE events DROP COLUMN location;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON COLUMN events.establishment_id IS 'Référence vers l''établissement où se déroule l''événement (v2.0)';
|
||||
COMMENT ON COLUMN events.is_private IS 'Indique si l''événement est privé (v2.0)';
|
||||
COMMENT ON COLUMN events.waitlist_enabled IS 'Indique si la liste d''attente est activée (v2.0)';
|
||||
|
||||
97
src/main/resources/db/migration/V8__Create_Reviews_Table.sql
Normal file
97
src/main/resources/db/migration/V8__Create_Reviews_Table.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
-- Migration V8: Création de la table reviews pour remplacer establishment_ratings
|
||||
-- Date: 2026-01-15
|
||||
-- Description: Nouvelle table pour les avis détaillés avec critères (v2.0)
|
||||
|
||||
-- Note: Cette migration crée une nouvelle table "reviews" qui remplace "establishment_ratings"
|
||||
-- La table establishment_ratings peut être conservée pour compatibilité ou migrée progressivement
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reviews (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
establishment_id UUID NOT NULL,
|
||||
overall_rating INTEGER NOT NULL, -- Note globale (1-5)
|
||||
comment TEXT, -- Commentaire libre
|
||||
criteria_ratings JSONB DEFAULT '{}'::jsonb NOT NULL, -- Notes par critères (ex: {"ambiance": 4, "service": 5, "qualite": 4})
|
||||
is_verified_visit BOOLEAN DEFAULT false NOT NULL, -- Indique si l'avis est lié à une visite vérifiée (réservation complétée)
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
CONSTRAINT fk_reviews_user
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES users(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_reviews_establishment
|
||||
FOREIGN KEY (establishment_id)
|
||||
REFERENCES establishments(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT chk_reviews_overall_rating_range
|
||||
CHECK (overall_rating >= 1 AND overall_rating <= 5),
|
||||
|
||||
-- Un utilisateur ne peut avoir qu'un seul avis par établissement
|
||||
CONSTRAINT uq_reviews_user_establishment
|
||||
UNIQUE (user_id, establishment_id)
|
||||
);
|
||||
|
||||
-- Créer les index pour améliorer les performances
|
||||
CREATE INDEX IF NOT EXISTS idx_reviews_user
|
||||
ON reviews(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reviews_establishment
|
||||
ON reviews(establishment_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reviews_overall_rating
|
||||
ON reviews(establishment_id, overall_rating);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reviews_created_at
|
||||
ON reviews(created_at DESC);
|
||||
|
||||
-- Index GIN pour les requêtes JSONB sur criteria_ratings
|
||||
CREATE INDEX IF NOT EXISTS idx_reviews_criteria_ratings
|
||||
ON reviews USING GIN (criteria_ratings);
|
||||
|
||||
-- Index pour les avis vérifiés
|
||||
CREATE INDEX IF NOT EXISTS idx_reviews_verified_visit
|
||||
ON reviews(establishment_id, is_verified_visit)
|
||||
WHERE is_verified_visit = true;
|
||||
|
||||
-- Créer un trigger pour mettre à jour updated_at automatiquement
|
||||
CREATE OR REPLACE FUNCTION update_reviews_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_reviews_updated_at
|
||||
BEFORE UPDATE ON reviews
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_reviews_updated_at();
|
||||
|
||||
-- Créer une vue pour calculer les statistiques d'avis par établissement
|
||||
CREATE OR REPLACE VIEW establishment_review_stats AS
|
||||
SELECT
|
||||
establishment_id,
|
||||
COUNT(*) as total_reviews,
|
||||
AVG(overall_rating)::NUMERIC(3,2) as average_rating,
|
||||
COUNT(*) FILTER (WHERE is_verified_visit = true) as verified_reviews_count,
|
||||
COUNT(*) FILTER (WHERE overall_rating = 5) as five_star_count,
|
||||
COUNT(*) FILTER (WHERE overall_rating = 4) as four_star_count,
|
||||
COUNT(*) FILTER (WHERE overall_rating = 3) as three_star_count,
|
||||
COUNT(*) FILTER (WHERE overall_rating = 2) as two_star_count,
|
||||
COUNT(*) FILTER (WHERE overall_rating = 1) as one_star_count
|
||||
FROM reviews
|
||||
GROUP BY establishment_id;
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON TABLE reviews IS 'Avis détaillés sur les établissements (v2.0 - remplace establishment_ratings)';
|
||||
COMMENT ON COLUMN reviews.user_id IS 'Utilisateur qui a écrit l''avis';
|
||||
COMMENT ON COLUMN reviews.establishment_id IS 'Établissement concerné par l''avis';
|
||||
COMMENT ON COLUMN reviews.overall_rating IS 'Note globale sur 5';
|
||||
COMMENT ON COLUMN reviews.comment IS 'Commentaire libre de l''utilisateur';
|
||||
COMMENT ON COLUMN reviews.criteria_ratings IS 'Notes par critères en JSONB (ex: {"ambiance": 4, "service": 5})';
|
||||
COMMENT ON COLUMN reviews.is_verified_visit IS 'Indique si l''avis est lié à une visite vérifiée (réservation complétée)';
|
||||
COMMENT ON VIEW establishment_review_stats IS 'Statistiques d''avis par établissement (calculées automatiquement)';
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
-- Migration V9: Création de la table reactions pour remplacer les compteurs de réactions
|
||||
-- Date: 2026-01-15
|
||||
-- Description: Table pour gérer les réactions des utilisateurs (like, love, etc.) sur différents contenus (v2.0)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reactions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
reaction_type VARCHAR(20) NOT NULL, -- LIKE, LOVE, HAHA, WOW, SAD, ANGRY
|
||||
target_type VARCHAR(20) NOT NULL, -- POST, EVENT, COMMENT, REVIEW
|
||||
target_id UUID NOT NULL, -- ID du contenu ciblé (post_id, event_id, comment_id, review_id)
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
CONSTRAINT fk_reactions_user
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES users(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT chk_reactions_type
|
||||
CHECK (reaction_type IN ('LIKE', 'LOVE', 'HAHA', 'WOW', 'SAD', 'ANGRY')),
|
||||
|
||||
CONSTRAINT chk_reactions_target_type
|
||||
CHECK (target_type IN ('POST', 'EVENT', 'COMMENT', 'REVIEW')),
|
||||
|
||||
-- Un utilisateur ne peut avoir qu'une seule réaction par contenu
|
||||
-- (mais peut changer de type de réaction)
|
||||
CONSTRAINT uq_reactions_user_target
|
||||
UNIQUE (user_id, target_type, target_id)
|
||||
);
|
||||
|
||||
-- Créer les index pour améliorer les performances
|
||||
CREATE INDEX IF NOT EXISTS idx_reactions_user
|
||||
ON reactions(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reactions_target
|
||||
ON reactions(target_type, target_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reactions_type
|
||||
ON reactions(reaction_type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reactions_created_at
|
||||
ON reactions(created_at DESC);
|
||||
|
||||
-- Index composite pour les requêtes fréquentes (compter les réactions par type et cible)
|
||||
CREATE INDEX IF NOT EXISTS idx_reactions_target_type
|
||||
ON reactions(target_type, target_id, reaction_type);
|
||||
|
||||
-- Créer une vue pour compter les réactions par type et cible
|
||||
CREATE OR REPLACE VIEW reaction_counts AS
|
||||
SELECT
|
||||
target_type,
|
||||
target_id,
|
||||
reaction_type,
|
||||
COUNT(*) as count
|
||||
FROM reactions
|
||||
GROUP BY target_type, target_id, reaction_type;
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON TABLE reactions IS 'Réactions des utilisateurs sur différents contenus (v2.0 - remplace les compteurs)';
|
||||
COMMENT ON COLUMN reactions.user_id IS 'Utilisateur qui a réagi';
|
||||
COMMENT ON COLUMN reactions.reaction_type IS 'Type de réaction: LIKE, LOVE, HAHA, WOW, SAD, ANGRY';
|
||||
COMMENT ON COLUMN reactions.target_type IS 'Type de contenu ciblé: POST, EVENT, COMMENT, REVIEW';
|
||||
COMMENT ON COLUMN reactions.target_id IS 'ID du contenu ciblé';
|
||||
COMMENT ON VIEW reaction_counts IS 'Compteurs de réactions par type et cible (calculés automatiquement)';
|
||||
|
||||
Reference in New Issue
Block a user