Erweiterungen

This commit is contained in:
2025-09-13 22:07:01 +02:00
parent f4656cd193
commit 5adfb9c2db
8 changed files with 801 additions and 347 deletions

View File

@@ -1,302 +1,177 @@
# STOMP Messaging Integration
# VOTIANLT Realtime & Photo Upload API
Die Anwendung unterstützt jetzt STOMP (Simple Text Oriented Messaging Protocol) für die Kommunikation mit externen Apps über WebSocket-Verbindungen.
This document describes how mobile/Flutter apps can interact with the backend via STOMP (WebSocket) and how to upload photos via HTTP POST (new flow).
## Übersicht
## 1) STOMP Overview
Das System bietet folgende STOMP-Funktionalitäten:
- Connect to one of the WebSocket endpoints:
- `/ws` (SockJS fallback supported)
- `/websocket` (native WebSocket)
- `/stomp` (native WebSocket, alternate path)
- Application destination prefix: `/app`
- Broker destinations:
- Broadcast: `/topic/...`
- User specific: `/user/queue/...`
### WebSocket-Endpunkte
Examples:
- Send a generic message: send to `/app/message`, receive on `/topic/messages`
- Job updates: send to `/app/job/status`, receive on `/topic/job-updates`
- Device locations: send to `/app/device/location`, receive on `/topic/device-locations`
- Task updates (broadcast): `/topic/task-updates`
- Task specific topic: `/topic/tasks/{taskId}`
- **`/ws`** - STOMP-Endpunkt mit SockJS-Fallback-Unterstützung
- **`/websocket`** - Reiner WebSocket-Endpunkt ohne SockJS
## 2) Photo Task New HTTP Upload Flow
### Nachrichtendestinationen
In the future, photos for a PHOTO task must NOT be embedded in the STOMP message anymore. Instead, upload the photos via HTTP POST and only send the task-completion event via STOMP without large payloads.
#### Eingehende Nachrichten (Client → Server)
- **`/app/message`** - Allgemeine Nachrichten
- **`/app/job/status`** - Job-Status-Updates
- **`/app/device/location`** - Gerätestandort-Updates
- **`/app/auth/login`** - Anmeldung eines App-Users (Payload: { email, password })
### Endpoint
#### Ausgehende Nachrichten (Server → Client)
- **`/topic/messages`** - Broadcast aller allgemeinen Nachrichten
- **`/topic/job-updates`** - Job-Status-Updates für alle Abonnenten
- **`/topic/device-locations`** - Gerätestandort-Updates
- **`/topic/broadcasts`** - System-weite Broadcast-Nachrichten
- **`/queue/notifications`** - Benutzerspezifische Benachrichtigungen
- URL: `POST /api/tasks/{taskId}/photos`
- Purpose: Upload one or many photos for a task of type `PHOTO`.
- Path params:
- `taskId` (required): The MongoDB ObjectId of the task.
- Auth: (same as your app uses today; if you have a session/cookie or token, include it accordingly)
- CORS: Enabled for `*` by default on this controller.
## Verwendung für Apps
### Content Types (choose one)
### 1. Verbindung aufbauen
1) Multipart form data (recommended for binary images)
```javascript
// Mit SockJS
const socket = new SockJS('http://192.168.180.196:8080/ws');
const stompClient = Stomp.over(socket);
- `Content-Type: multipart/form-data`
- Fields:
- `files`: one or multiple image files (repeat the field for multiple images)
- `completedBy` (optional): username/id of the completing user
- `note` (optional): any note (currently stored on the task when you mark it as completed via STOMP)
// Oder mit nativem WebSocket (WICHTIG: ws:// verwenden, nicht http://)
const socket = new WebSocket('ws://192.168.180.196:8080/websocket');
const stompClient = Stomp.over(socket);
Example (curl):
```bash
curl -X POST "http://localhost:8080/api/tasks/66f5c2f1a3b6e27a1a234567/photos" \
-H "Content-Type: multipart/form-data" \
-F "files=@/path/to/photo1.jpg" \
-F "files=@/path/to/photo2.jpg" \
-F "completedBy=driver01" \
-F "note=Anlieferung dokumentiert"
```
### Flutter/Dart STOMP Client
2) JSON with base64 strings (kept for compatibility)
**Für Flutter-Apps verwenden Sie diese Konfiguration:**
```dart
import 'package:stomp_dart_client/stomp.dart';
import 'package:stomp_dart_client/stomp_config.dart';
import 'package:stomp_dart_client/stomp_frame.dart';
import 'dart:convert';
// WICHTIG: Verwenden Sie ws:// für WebSocket-Verbindungen, NICHT http://
final stompClient = StompClient(
config: StompConfig(
url: 'ws://192.168.180.196:8080/websocket', // Beachten Sie ws:// statt http://
onConnect: onConnectCallback,
onWebSocketError: (dynamic error) => print('WebSocket Error: $error'),
onStompError: (StompFrame frame) => print('Stomp Error: ${frame.body}'),
onDisconnect: (StompFrame frame) => print('Disconnected'),
// Heartbeat-Konfiguration
heartbeatIncoming: Duration(seconds: 20),
heartbeatOutgoing: Duration(seconds: 20),
),
);
void onConnectCallback(StompFrame frame) {
print('Connected to STOMP server');
// Nachrichten abonnieren
stompClient.subscribe(
destination: '/topic/messages',
callback: (StompFrame frame) {
print('Received message: ${frame.body}');
},
);
}
// Verbindung herstellen
stompClient.activate();
// Nachricht senden
void sendMessage(String content) {
stompClient.send(
destination: '/app/message',
body: jsonEncode({
'content': content,
'sender': 'FlutterApp',
}),
);
}
```
**Alternative Endpunkte für Flutter:**
Falls Probleme mit `/websocket` auftreten, versuchen Sie:
- `ws://192.168.180.196:8080/stomp` (zusätzlicher Endpunkt)
- `ws://192.168.180.196:8080/ws` (SockJS-Endpunkt ohne SockJS-Protokoll)
### 2. Verbindung herstellen
```javascript
stompClient.connect({}, function(frame) {
console.log('Verbunden: ' + frame);
// Nachrichten abonnieren
stompClient.subscribe('/topic/messages', function(message) {
console.log('Nachricht erhalten:', JSON.parse(message.body));
});
});
```
### 3. Nachrichten senden
```javascript
// Allgemeine Nachricht senden
stompClient.send('/app/message', {}, JSON.stringify({
content: 'Hallo vom App',
sender: 'MobileApp'
}));
// Job-Status-Update senden
stompClient.send('/app/job/status', {}, JSON.stringify({
jobId: '12345',
status: 'IN_PROGRESS',
progress: 75
}));
// Gerätestandort senden
stompClient.send('/app/device/location', {}, JSON.stringify({
deviceId: 'device-001',
latitude: 52.5200,
longitude: 13.4050,
accuracy: 10
}));
// Anmeldung eines App-Users
// Zuerst die Antwort-Warteschlange abonnieren (user-spezifisch)
const authSubscription = stompClient.subscribe('/user/queue/auth', function(message) {
const resp = JSON.parse(message.body);
console.log('Login-Antwort:', resp);
});
// Login-Request senden
stompClient.send('/app/auth/login', {}, JSON.stringify({
email: 'user@example.com',
password: 'geheimesPasswort'
}));
```
## Backend-Integration
### Programmatische Nachrichten senden
```java
@Autowired
private MessageController messageController;
// Benachrichtigung an spezifischen Benutzer
messageController.sendNotificationToUser("username", "Neue Aufgabe verfügbar");
// Broadcast-Nachricht an alle
messageController.sendBroadcastMessage("Systemwartung in 10 Minuten");
```
## Zeroconf (mDNS) Veröffentlichung
Die Anwendung veröffentlicht die STOMP-Schnittstelle via Zeroconf (DNS-SD/mDNS), sofern verfügbar. Es wird der Service-Typ `_stomp._tcp.local.` mit folgenden TXT-Records publiziert:
- path = Pfad für SockJS-Endpoint (Standard: /ws)
- websocket = Pfad für nativen WebSocket (Standard: /websocket)
- protocol = "stomp"
Clients können per Bonjour/mDNS nach `_stomp._tcp` suchen und erhalten Port und Metadaten.
Hinweise:
- Die Implementierung nutzt JmDNS, falls die Bibliothek auf dem Klassenpfad vorhanden ist. In Umgebungen ohne JmDNS bleibt Zeroconf stillschweigend deaktiviert (es wird ein Hinweis im Log ausgegeben).
- Konfigurierbare Properties:
- app.zeroconf.enabled (default: true)
- app.zeroconf.serviceName (default: votianlt-stomp)
- app.stomp.wsPath (default: /ws)
- app.stomp.websocketPath (default: /websocket)
## Konfiguration
Die STOMP-Konfiguration befindet sich in:
- **`WebSocketConfig.java`** - WebSocket und STOMP-Konfiguration
- **`MessageController.java`** - Nachrichtenbehandlung
- **`application.properties`** - Zusätzliche WebSocket-Einstellungen
### Wichtige Konfigurationsparameter
```properties
# Nachrichtenpuffergröße
spring.websocket.servlet.max-text-message-buffer-size=8192
spring.websocket.servlet.max-binary-message-buffer-size=8192
# STOMP aktivieren
spring.websocket.stomp.enabled=true
# Heartbeat-Einstellungen
spring.websocket.stomp.heartbeat.outgoing=10000
spring.websocket.stomp.heartbeat.incoming=10000
```
## Sicherheitshinweise
- WebSocket-Verbindungen verwenden die gleiche Authentifizierung wie die Web-Anwendung
- Nachrichten werden automatisch mit Zeitstempel versehen
- Alle Nachrichten werden in JSON-Format verarbeitet
## Testing
Zum Testen der STOMP-Funktionalität können Sie:
1. Eine WebSocket-Client-Bibliothek verwenden
2. Browser-Entwicklertools für WebSocket-Verbindungen nutzen
3. Spezialisierte STOMP-Testing-Tools einsetzen
Die Implementierung ist vollständig und bereit für die Integration mit externen Apps.
## Neue STOMP-Schnittstelle: Task-Erledigung melden
Mit dieser Schnittstelle kann ein Client die Erledigung eines Tasks melden.
- Senden (Client → Server): `/app/task/completed`
- Broadcasts (Server → Client):
- Global: `/topic/task-updates`
- Task-spezifisch: `/topic/tasks/{taskId}`
Payload (JSON):
- `Content-Type: application/json`
- Body:
```json
{
"taskId": "<MongoId als String>",
"completedBy": "<optional: Name/ID des Ausführenden>",
"note": "<optional: Kommentar>"
}
```
Antwort (Beispiel):
```json
{
"timestamp": "2025-09-05T09:25:00",
"type": "taskCompletedAck",
"success": true,
"taskId": "...",
"jobId": "...",
"completed": true,
"completedAt": "2025-09-05T09:25:00",
"completedBy": "driver01",
"note": "Übergabe erfolgreich",
"event": "taskCompleted"
"note": "Anlieferung dokumentiert",
"photos": [
"<base64-of-image-1>",
"<base64-of-image-2>"
]
}
```
JavaScript Beispiel:
Example (curl):
```javascript
// Abonnieren der globalen Updates
stompClient.subscribe('/topic/task-updates', (frame) => {
console.log('Task update:', JSON.parse(frame.body));
});
// Abonnieren eines spezifischen Tasks
stompClient.subscribe('/topic/tasks/' + taskId, (frame) => {
console.log('Task-specific update:', JSON.parse(frame.body));
});
// Task als erledigt melden
stompClient.send('/app/task/completed', {}, JSON.stringify({
taskId: taskId,
completedBy: 'driver01',
note: 'Übergabe erfolgreich'
}));
```bash
curl -X POST "http://localhost:8080/api/tasks/66f5c2f1a3b6e27a1a234567/photos" \
-H "Content-Type: application/json" \
-d '{
"completedBy":"driver01",
"note":"Anlieferung dokumentiert",
"photos":["iVBORw0KGgo...","iVBORw0KGgo..."]
}'
```
Flutter/Dart Beispiel:
### Simple JSON endpoint (single photo per call)
```dart
stompClient.subscribe(
destination: '/topic/task-updates',
callback: (frame) => print('Task update: ${frame.body}'),
);
- URL: `POST /api/photos`
- Content-Type: `application/json`
- Body:
stompClient.subscribe(
destination: '/topic/tasks/$taskId',
callback: (frame) => print('Task-specific update: ${frame.body}'),
);
stompClient.send(
destination: '/app/task/completed',
body: jsonEncode({
'taskId': taskId,
'completedBy': 'driver01',
'note': 'Übergabe erfolgreich',
}),
);
```json
{
"taskId": "66f5c2f1a3b6e27a1a234567",
"photo": "<base64-of-image>",
"completedBy": "driver01",
"note": "optional"
}
```
Hinweise:
- `taskId` ist Pflicht. Bei ungültiger oder unbekannter `taskId` wird `success=false` zurückgegeben.
- Der Server setzt `completed=true` und `completedAt` automatisch.
- Zusätzlich zum globalen Broadcast wird ein task-spezifisches Event auf `/topic/tasks/{taskId}` versendet.
- Multiple photos: call this endpoint multiple times (one image per request).
Example (curl):
```bash
curl -X POST "http://localhost:8080/api/photos" \
-H "Content-Type: application/json" \
-d '{
"taskId":"66f5c2f1a3b6e27a1a234567",
"photo":"iVBORw0KGgo...",
"completedBy":"driver01",
"note":"Anlieferung dokumentiert"
}'
```
### Response
Both content types return the same JSON structure on success:
```json
{
"timestamp": "2025-09-13T21:27:00",
"type": "photoUploadAck",
"success": true,
"photoId": "66f5c400b1a1b05b1c123abc",
"photosCount": 2,
"taskId": "66f5c2f1a3b6e27a1a234567",
"jobId": "66f5c2f1a3b6e27a1a200000"
}
```
Error responses use HTTP status codes with:
```json
{ "timestamp": "...", "type": "photoUploadAck", "success": false, "message": "..." }
```
### Server-side side-effect (notification)
After a successful upload, a small event is broadcast to the specific task topic:
- Destination: `/topic/tasks/{taskId}`
- Payload:
```json
{ "event": "photoUploaded", "taskId": "...", "jobId": "...", "photosCount": 2, "timestamp": "..." }
```
Use this to update UI (e.g., show that photos have arrived) without transferring large images via STOMP.
### Limits
- Multipart limits (configurable in `application.properties`):
- `spring.servlet.multipart.max-file-size=32MB`
- `spring.servlet.multipart.max-request-size=64MB`
- Tomcat limits adjusted accordingly.
If you need higher limits, we can raise these values.
## 3) Marking Task as Completed (STOMP)
Continue to mark a task as completed via STOMP without embedding images:
- Send to: `/app/task/photo/completed`
- Payload (no photos inside):
```json
{ "taskId": "66f5c2f1a3b6e27a1a234567", "completedBy": "driver01", "note": "Anlieferung dokumentiert" }
```
- Broadcasts an update to `/topic/task-updates` and `/topic/tasks/{taskId}`.
## 4) Retrieval of Photos (optional)
Currently, the backend stores uploaded photos as base64 in the `photos` collection referencing the `taskId` and `jobId`. If you need a download/list endpoint, let us know—we can expose `/api/tasks/{taskId}/photos` (GET) to return metadata and/or images.
---
If anything is unclear or you need different formats (e.g., presigned URLs, chunked uploads), please reach out.