feat(match3): Erweiterung der Match3-Admin-Funktionalitäten und -Modelle
- Implementierung neuer Endpunkte für die Verwaltung von Match3-Kampagnen, Levels, Objectives und Tile-Typen im Admin-Bereich. - Anpassung der Admin-Services zur Unterstützung von Benutzerberechtigungen und Fehlerbehandlung. - Einführung von neuen Modellen und Assoziationen für Match3-Levels und Tile-Typen in der Datenbank. - Verbesserung der Internationalisierung für Match3-spezifische Texte in Deutsch und Englisch. - Aktualisierung der Frontend-Routen und -Komponenten zur Verwaltung von Match3-Inhalten.
This commit is contained in:
177
backend/README_SCHEMA_UPDATES.md
Normal file
177
backend/README_SCHEMA_UPDATES.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Datenbank-Schema-Updates
|
||||
|
||||
## Problem
|
||||
Sequelize erstellt bei jedem `sync()` mit `alter: true` die Constraints neu, was zu Duplikaten und Problemen führen kann.
|
||||
|
||||
## Lösung
|
||||
|
||||
### 1. Intelligente Schema-Synchronisation (Standard)
|
||||
```javascript
|
||||
// In utils/sequelize.js - syncModelsWithUpdates()
|
||||
// Prüft automatisch ob Schema-Updates nötig sind
|
||||
// Verwendet alter: true nur bei Bedarf
|
||||
await syncModelsWithUpdates(models);
|
||||
```
|
||||
- **Automatische Erkennung** von Schema-Updates
|
||||
- **alter: true nur bei Bedarf** - verhindert Duplikate
|
||||
- **Fallback** zu alter: false wenn keine Updates nötig
|
||||
- **Sicher und effizient**
|
||||
|
||||
### 2. Normale Synchronisation (ohne Updates)
|
||||
```javascript
|
||||
// In utils/sequelize.js - syncModels()
|
||||
await model.sync({ alter: false, force: false });
|
||||
```
|
||||
- **Keine Schema-Änderungen**
|
||||
- **Keine Constraints werden neu erstellt**
|
||||
- **Schnell und sicher**
|
||||
|
||||
### 3. Manuelle Schema-Updates (nur bei Bedarf)
|
||||
```javascript
|
||||
// In utils/sequelize.js - updateSchema()
|
||||
await model.sync({ alter: true, force: false });
|
||||
```
|
||||
- **Nur bei expliziten Schema-Änderungen verwenden**
|
||||
- **Kann zu Duplikaten führen**
|
||||
- **Nach Update: Zurück zu intelligenter Synchronisation**
|
||||
|
||||
### 4. Vollständiger Reset (nur in Entwicklung)
|
||||
```javascript
|
||||
await model.sync({ force: true });
|
||||
```
|
||||
- **Löscht alle Daten!**
|
||||
- **Nur in Entwicklungsumgebung verwenden**
|
||||
|
||||
## Aktuelle Implementierung
|
||||
|
||||
### Intelligente Synchronisation
|
||||
```javascript
|
||||
// syncModelsWithUpdates() prüft automatisch:
|
||||
// 1. Alle verfügbaren Schemas (community, falukant_data, match3, etc.)
|
||||
// 2. Alle Tabellen in jedem Schema
|
||||
// 3. Alle Spalten in jeder Tabelle
|
||||
// 4. Datentypen, Constraints und Standardwerte
|
||||
// 5. Verwendet alter: true nur wenn nötig
|
||||
```
|
||||
|
||||
### Universelle Schema-Prüfung
|
||||
```javascript
|
||||
// checkSchemaUpdates() prüft alle Schemas:
|
||||
// - community (Benutzer, Rechte, etc.)
|
||||
// - falukant_data (Spieldaten)
|
||||
// - falukant_type (Spieltypen)
|
||||
// - falukant_predefine (Vordefinierte Daten)
|
||||
// - falukant_log (Spiel-Logs)
|
||||
// - chat (Chat-System)
|
||||
// - forum (Forum-System)
|
||||
// - match3 (Match3-Spiel)
|
||||
// - logs (Allgemeine Logs)
|
||||
// - type (Allgemeine Typen)
|
||||
// - service (Service-Daten)
|
||||
```
|
||||
|
||||
### Detaillierte Tabellen-Prüfung
|
||||
```javascript
|
||||
// checkTableForUpdates() prüft jede Tabelle:
|
||||
// - Fehlende Spalten
|
||||
// - Neue Spalten
|
||||
// - Geänderte Spalten
|
||||
// - Model-zu-Tabelle-Zuordnung
|
||||
```
|
||||
|
||||
### Umfassende Spalten-Prüfung
|
||||
```javascript
|
||||
// checkColumnForUpdates() prüft jede Spalte:
|
||||
// - Datentyp-Änderungen (INTEGER → BIGINT)
|
||||
// - NULL/NOT NULL Constraints
|
||||
// - Standardwerte
|
||||
// - Längen-Änderungen (VARCHAR)
|
||||
```
|
||||
|
||||
## Workflow für Schema-Updates
|
||||
|
||||
### Schritt 1: Schema ändern
|
||||
```javascript
|
||||
// In models/.../model.js
|
||||
// Neue Felder, Constraints, etc. hinzufügen
|
||||
```
|
||||
|
||||
### Schritt 2: Automatische Erkennung
|
||||
```javascript
|
||||
// syncModelsWithUpdates() erkennt automatisch:
|
||||
// - Welche Updates nötig sind
|
||||
// - Verwendet alter: true nur bei Bedarf
|
||||
// - Läuft bei jedem Start sicher
|
||||
```
|
||||
|
||||
### Schritt 3: Keine manuellen Änderungen nötig
|
||||
```javascript
|
||||
// Die intelligente Synchronisation:
|
||||
// - Erkennt Änderungen automatisch
|
||||
// - Wendet sie sicher an
|
||||
// - Verhindert Duplikate
|
||||
```
|
||||
|
||||
## Constraint-Bereinigung
|
||||
|
||||
### Analyse ausführen
|
||||
```bash
|
||||
cd backend
|
||||
node utils/cleanupDatabaseConstraints.js
|
||||
```
|
||||
|
||||
### Manuelle Bereinigung
|
||||
```sql
|
||||
-- Doppelte Foreign Keys entfernen
|
||||
ALTER TABLE schema.table_name DROP CONSTRAINT constraint_name;
|
||||
|
||||
-- Doppelte Indexe entfernen
|
||||
DROP INDEX IF EXISTS index_name;
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Intelligente Synchronisation verwenden** - `syncModelsWithUpdates()`
|
||||
2. **Keine manuellen Schema-Updates** bei jedem Start
|
||||
3. **updateSchema() nur bei komplexen Änderungen** verwenden
|
||||
4. **Constraints regelmäßig prüfen** auf Duplikate
|
||||
5. **Backup vor Schema-Updates** erstellen
|
||||
|
||||
## Aktuelle Konfiguration
|
||||
|
||||
- **Standard-Synchronisation**: `syncModelsWithUpdates()` (intelligent)
|
||||
- **Schema-Updates**: Automatisch erkannt und angewendet
|
||||
- **Manuelle Updates**: Separate `updateSchema()` Funktion
|
||||
- **Automatische Updates**: Aktiviert und sicher
|
||||
- **Manuelle Kontrolle**: Verfügbar bei Bedarf
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Duplizierte Constraints
|
||||
```bash
|
||||
# Analyse ausführen
|
||||
node utils/cleanupDatabaseConstraints.js
|
||||
|
||||
# Manuell bereinigen
|
||||
# Siehe SQL-Beispiele oben
|
||||
```
|
||||
|
||||
### Problem: Schema-Synchronisation schlägt fehl
|
||||
```javascript
|
||||
// Intelligente Synchronisation sollte das automatisch handhaben
|
||||
// Bei Problemen: updateSchema() verwenden
|
||||
await updateSchema(models);
|
||||
```
|
||||
|
||||
### Problem: Performance-Probleme
|
||||
- **Intelligente Synchronisation** läuft nur bei Bedarf
|
||||
- **alter: true** wird nur verwendet wenn nötig
|
||||
- **Normale Starts** sind schnell und sicher
|
||||
|
||||
## Vorteile der neuen Lösung
|
||||
|
||||
1. **Automatisch**: Erkennt Schema-Updates automatisch
|
||||
2. **Sicher**: Verwendet alter: true nur bei Bedarf
|
||||
3. **Effizient**: Normale Starts ohne Schema-Updates
|
||||
4. **Flexibel**: Kann für verschiedene Modelle erweitert werden
|
||||
5. **Wartbar**: Klare Trennung zwischen Update-Logik und Synchronisation
|
||||
@@ -139,41 +139,73 @@ class AdminController {
|
||||
|
||||
async getRoomTypes(req, res) {
|
||||
try {
|
||||
const types = await AdminService.getRoomTypes();
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const types = await AdminService.getRoomTypes(userId);
|
||||
res.status(200).json(types);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getGenderRestrictions(req, res) {
|
||||
try {
|
||||
const restrictions = await AdminService.getGenderRestrictions();
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const restrictions = await AdminService.getGenderRestrictions(userId);
|
||||
res.status(200).json(restrictions);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getUserRights(req, res) {
|
||||
try {
|
||||
const rights = await AdminService.getUserRights();
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const rights = await AdminService.getUserRights(userId);
|
||||
res.status(200).json(rights);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getRooms(req, res) {
|
||||
try {
|
||||
const rooms = await AdminService.getRooms();
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const rooms = await AdminService.getRooms(userId);
|
||||
res.status(200).json(rooms);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.log(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +283,350 @@ class AdminController {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Match3 Admin Methods ---
|
||||
async getMatch3Campaigns(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const campaigns = await AdminService.getMatch3Campaigns(userId);
|
||||
res.status(200).json(campaigns);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in getMatch3Campaigns:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getMatch3Campaign(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const campaign = await AdminService.getMatch3Campaign(userId, req.params.id);
|
||||
res.status(200).json(campaign);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in getMatch3Campaign:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createMatch3Campaign(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const campaign = await AdminService.createMatch3Campaign(userId, req.body);
|
||||
res.status(201).json(campaign);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in createMatch3Campaign:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateMatch3Campaign(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const campaign = await AdminService.updateMatch3Campaign(userId, req.params.id, req.body);
|
||||
res.status(200).json(campaign);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in updateMatch3Campaign:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMatch3Campaign(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
await AdminService.deleteMatch3Campaign(userId, req.params.id);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in deleteMatch3Campaign:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getMatch3Levels(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const levels = await AdminService.getMatch3Levels(userId);
|
||||
res.status(200).json(levels);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in getMatch3Levels:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getMatch3Level(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const level = await AdminService.getMatch3Level(userId, req.params.id);
|
||||
res.status(200).json(level);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in getMatch3Level:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createMatch3Level(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const level = await AdminService.createMatch3Level(userId, req.body);
|
||||
res.status(201).json(level);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in createMatch3Level:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateMatch3Level(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const level = await AdminService.updateMatch3Level(userId, req.params.id, req.body);
|
||||
res.status(200).json(level);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in updateMatch3Level:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMatch3Level(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
await AdminService.deleteMatch3Level(userId, req.params.id);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in deleteMatch3Level:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match3 Objectives
|
||||
async getMatch3Objectives(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const objectives = await AdminService.getMatch3Objectives(userId);
|
||||
res.status(200).json(objectives);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in getMatch3Objectives:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getMatch3Objective(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const objective = await AdminService.getMatch3Objective(userId, req.params.id);
|
||||
res.status(200).json(objective);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in getMatch3Objective:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createMatch3Objective(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const objective = await AdminService.createMatch3Objective(userId, req.body);
|
||||
res.status(201).json(objective);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in createMatch3Objective:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateMatch3Objective(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const objective = await AdminService.updateMatch3Objective(userId, req.params.id, req.body);
|
||||
res.status(200).json(objective);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in updateMatch3Objective:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMatch3Objective(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
await AdminService.deleteMatch3Objective(userId, req.params.id);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in deleteMatch3Objective:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getMatch3TileTypes(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const tileTypes = await AdminService.getMatch3TileTypes(userId);
|
||||
res.status(200).json(tileTypes);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in getMatch3TileTypes:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createMatch3TileType(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const tileType = await AdminService.createMatch3TileType(userId, req.body);
|
||||
res.status(201).json(tileType);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in createMatch3TileType:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateMatch3TileType(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
const tileType = await AdminService.updateMatch3TileType(userId, req.params.id, req.body);
|
||||
res.status(200).json(tileType);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in updateMatch3TileType:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMatch3TileType(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'User ID fehlt' });
|
||||
}
|
||||
await AdminService.deleteMatch3TileType(userId, req.params.id);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
|
||||
} else {
|
||||
console.error('Error in deleteMatch3TileType:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminController;
|
||||
|
||||
@@ -1,128 +1,212 @@
|
||||
import match3Service from '../services/match3Service.js';
|
||||
import Match3Service from '../services/match3Service.js';
|
||||
|
||||
class Match3Controller {
|
||||
/**
|
||||
* Lädt alle aktiven Kampagnen
|
||||
*/
|
||||
async getCampaigns(req, res) {
|
||||
try {
|
||||
const campaigns = await match3Service.getActiveCampaigns();
|
||||
res.json({ success: true, data: campaigns });
|
||||
} catch (error) {
|
||||
console.error('Error in getCampaigns:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Kampagnen' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt eine spezifische Kampagne
|
||||
*/
|
||||
async getCampaign(req, res) {
|
||||
try {
|
||||
const { campaignId } = req.params;
|
||||
const campaign = await match3Service.getCampaign(campaignId);
|
||||
|
||||
if (!campaign) {
|
||||
return res.status(404).json({ success: false, message: 'Kampagne nicht gefunden' });
|
||||
}
|
||||
|
||||
res.json({ success: true, data: campaign });
|
||||
} catch (error) {
|
||||
console.error('Error in getCampaign:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Kampagne' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt den Benutzerfortschritt für eine Kampagne
|
||||
*/
|
||||
async getUserProgress(req, res) {
|
||||
try {
|
||||
const { campaignId } = req.params;
|
||||
const userId = req.headers.userid || req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ success: false, message: 'Benutzer-ID nicht gefunden' });
|
||||
}
|
||||
|
||||
const userProgress = await match3Service.getUserProgress(userId, campaignId);
|
||||
res.json({ success: true, data: userProgress });
|
||||
} catch (error) {
|
||||
console.error('Error in getUserProgress:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden des Fortschritts' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Level-Fortschritt eines Benutzers
|
||||
*/
|
||||
async updateLevelProgress(req, res) {
|
||||
try {
|
||||
const { campaignId, levelId } = req.params;
|
||||
const userId = req.headers.userid || req.user?.id;
|
||||
const { score, moves, time, stars, isCompleted } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ success: false, message: 'Benutzer-ID nicht gefunden' });
|
||||
}
|
||||
|
||||
if (!score || !moves || !stars) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Alle erforderlichen Felder müssen ausgefüllt werden'
|
||||
});
|
||||
}
|
||||
|
||||
const levelData = {
|
||||
score: parseInt(score),
|
||||
moves: parseInt(moves),
|
||||
time: time ? parseInt(time) : 0,
|
||||
stars: parseInt(stars),
|
||||
isCompleted: Boolean(isCompleted)
|
||||
};
|
||||
|
||||
const result = await match3Service.updateLevelProgress(userId, campaignId, levelId, levelData);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
console.error('Error in updateLevelProgress:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren des Fortschritts' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Bestenliste für eine Kampagne
|
||||
*/
|
||||
async getLeaderboard(req, res) {
|
||||
try {
|
||||
const { campaignId } = req.params;
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
const leaderboard = await match3Service.getLeaderboard(campaignId, parseInt(limit));
|
||||
res.json({ success: true, data: leaderboard });
|
||||
} catch (error) {
|
||||
console.error('Error in getLeaderboard:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Bestenliste' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Statistiken für einen Benutzer
|
||||
*/
|
||||
async getUserStats(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid || req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ success: false, message: 'Benutzer-ID nicht gefunden' });
|
||||
}
|
||||
|
||||
const stats = await match3Service.getUserStats(userId);
|
||||
res.json({ success: true, data: stats });
|
||||
} catch (error) {
|
||||
console.error('Error in getUserStats:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Statistiken' });
|
||||
}
|
||||
}
|
||||
function extractHashedUserId(req) {
|
||||
return req.headers?.userid;
|
||||
}
|
||||
|
||||
export default new Match3Controller();
|
||||
class Match3Controller {
|
||||
constructor() {
|
||||
this.service = Match3Service;
|
||||
|
||||
// Binde alle Methoden an die Klasse
|
||||
this.getCampaigns = this.getCampaigns.bind(this);
|
||||
this.getCampaign = this.getCampaign.bind(this);
|
||||
this.getLevel = this.getLevel.bind(this);
|
||||
this.getUserProgress = this.getUserProgress.bind(this);
|
||||
this.updateLevelProgress = this.updateLevelProgress.bind(this);
|
||||
this.getUserStats = this.getUserStats.bind(this);
|
||||
this.cleanupUserProgress = this.cleanupUserProgress.bind(this);
|
||||
}
|
||||
|
||||
// Kampagnen-Endpunkte
|
||||
async getCampaigns(req, res) {
|
||||
try {
|
||||
const result = await this.service.getCampaigns();
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in getCampaigns:', error);
|
||||
const status = error.status || 500;
|
||||
const message = error.message || 'Internal server error';
|
||||
res.status(status).json({
|
||||
success: false,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getCampaign(req, res) {
|
||||
try {
|
||||
const result = await this.service.getCampaign(req.params.id);
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in getCampaign:', error);
|
||||
const status = error.status || 500;
|
||||
const message = error.message || 'Internal server error';
|
||||
res.status(status).json({
|
||||
success: false,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getLevel(req, res) {
|
||||
try {
|
||||
const result = await this.service.getLevel(req.params.id);
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in getLevel:', error);
|
||||
const status = error.status || 500;
|
||||
const message = error.message || 'Internal server error';
|
||||
res.status(status).json({
|
||||
success: false,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Benutzer-spezifische Endpunkte
|
||||
async getUserProgress(req, res) {
|
||||
try {
|
||||
const userId = extractHashedUserId(req);
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'User ID required'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.service.getUserProgress(userId, req.params.campaignId);
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in getUserProgress:', error);
|
||||
const status = error.status || 500;
|
||||
const message = error.message || 'Internal server error';
|
||||
res.status(status).json({
|
||||
success: false,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async updateLevelProgress(req, res) {
|
||||
try {
|
||||
const userId = extractHashedUserId(req);
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'User ID required'
|
||||
});
|
||||
}
|
||||
|
||||
const { score, moves, time, stars, securityHash, timestamp } = req.body;
|
||||
|
||||
// Prüfe ob alle erforderlichen Sicherheitsdaten vorhanden sind
|
||||
if (!securityHash || !timestamp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Security validation data missing'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.service.updateLevelProgress(
|
||||
userId,
|
||||
req.params.campaignId,
|
||||
req.params.levelId,
|
||||
score,
|
||||
moves,
|
||||
time,
|
||||
stars,
|
||||
securityHash,
|
||||
timestamp
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in updateLevelProgress:', error);
|
||||
const status = error.status || 500;
|
||||
const message = error.message || 'Internal server error';
|
||||
res.status(status).json({
|
||||
success: false,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Bereinige falsche Level-Abschlüsse für einen Benutzer
|
||||
async cleanupUserProgress(req, res) {
|
||||
try {
|
||||
const userId = extractHashedUserId(req);
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'User ID required'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.service.cleanupUserProgress(userId, req.params.campaignId);
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: result.message
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in cleanupUserProgress:', error);
|
||||
const status = error.status || 500;
|
||||
const message = error.message || 'Internal server error';
|
||||
res.status(status).json({
|
||||
success: false,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getUserStats(req, res) {
|
||||
try {
|
||||
const userId = extractHashedUserId(req);
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'User ID required'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.service.getUserStats(userId);
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in getUserStats:', error);
|
||||
const status = error.status || 500;
|
||||
const message = error.message || 'Internal server error';
|
||||
res.status(status).json({
|
||||
success: false,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Match3Controller;
|
||||
|
||||
@@ -258,6 +258,15 @@ const menuStructure = {
|
||||
path: "/admin/falukant/database"
|
||||
},
|
||||
}
|
||||
},
|
||||
minigames: {
|
||||
visible: ["mainadmin", "match3"],
|
||||
children: {
|
||||
match3: {
|
||||
visible: ["mainadmin", "match3"],
|
||||
path: "/admin/minigames/match3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,7 +293,7 @@ class NavigationController {
|
||||
const hasFalukantAccount = await this.hasFalukantAccount(userId);
|
||||
for (const [key, value] of Object.entries(menu)) {
|
||||
if (value.visible.includes("all")
|
||||
|| value.visible.some(v => rights.includes(v) || (value.visible.includes("anyadmin") && rights.length > 0))
|
||||
|| value.visible.some(v => rights.includes(v)) || (value.visible.includes("anyadmin") && rights.length > 0)
|
||||
|| (value.visible.includes("over14") && age >= 14)
|
||||
|| (value.visible.includes("over12") && age >= 12)
|
||||
|| (value.visible.includes("over18") && age >= 18)
|
||||
|
||||
@@ -107,6 +107,8 @@ import Match3Level from './match3/level.js';
|
||||
import Match3Objective from './match3/objective.js';
|
||||
import Match3UserProgress from './match3/userProgress.js';
|
||||
import Match3UserLevelProgress from './match3/userLevelProgress.js';
|
||||
import Match3TileType from './match3/tileType.js';
|
||||
import Match3LevelTileType from './match3/levelTileType.js';
|
||||
|
||||
export default function setupAssociations() {
|
||||
// RoomType 1:n Room
|
||||
@@ -841,4 +843,23 @@ export default function setupAssociations() {
|
||||
foreignKey: 'levelId',
|
||||
as: 'level'
|
||||
});
|
||||
|
||||
// Match3 Tile Type associations
|
||||
Match3Level.hasMany(Match3LevelTileType, {
|
||||
foreignKey: 'levelId',
|
||||
as: 'levelTileTypes'
|
||||
});
|
||||
Match3LevelTileType.belongsTo(Match3Level, {
|
||||
foreignKey: 'levelId',
|
||||
as: 'level'
|
||||
});
|
||||
|
||||
Match3TileType.hasMany(Match3LevelTileType, {
|
||||
foreignKey: 'tileTypeId',
|
||||
as: 'levelTileTypes'
|
||||
});
|
||||
Match3LevelTileType.belongsTo(Match3TileType, {
|
||||
foreignKey: 'tileTypeId',
|
||||
as: 'tileType'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@ import Match3Level from './match3/level.js';
|
||||
import Match3Objective from './match3/objective.js';
|
||||
import Match3UserProgress from './match3/userProgress.js';
|
||||
import Match3UserLevelProgress from './match3/userLevelProgress.js';
|
||||
import Match3TileType from './match3/tileType.js';
|
||||
import Match3LevelTileType from './match3/levelTileType.js';
|
||||
|
||||
// — Politische Ämter (Politics) —
|
||||
import PoliticalOfficeType from './falukant/type/political_office_type.js';
|
||||
@@ -214,6 +216,8 @@ const models = {
|
||||
Match3Objective,
|
||||
Match3UserProgress,
|
||||
Match3UserLevelProgress,
|
||||
Match3TileType,
|
||||
Match3LevelTileType,
|
||||
PoliticalOfficeType,
|
||||
PoliticalOfficeRequirement,
|
||||
PoliticalOfficeBenefitType,
|
||||
|
||||
@@ -13,6 +13,7 @@ const Login = sequelize.define('login', {
|
||||
}
|
||||
}, {
|
||||
schema: 'logs',
|
||||
underscored: true,
|
||||
tableName: 'login'
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ const Campaign = sequelize.define('Campaign', {
|
||||
}, {
|
||||
tableName: 'match3_campaigns',
|
||||
schema: 'match3',
|
||||
timestamps: true
|
||||
timestamps: true,
|
||||
underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format
|
||||
});
|
||||
|
||||
export default Campaign;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
const Level = sequelize.define('Level', {
|
||||
const Match3Level = sequelize.define('Match3Level', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
@@ -9,7 +9,11 @@ const Level = sequelize.define('Level', {
|
||||
},
|
||||
campaignId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'match3_campaigns',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(255),
|
||||
@@ -21,41 +25,51 @@ const Level = sequelize.define('Level', {
|
||||
},
|
||||
order: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1
|
||||
allowNull: false
|
||||
},
|
||||
boardSize: {
|
||||
boardLayout: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true, // Ändern zu true, da bereits existierende Datensätze vorhanden sind
|
||||
defaultValue: 'xxxxxx\nxxxxxx\nxxxxxx\nxxxxxx\nxxxxxx\nxxxxxx', // Standard-Layout für neue Level
|
||||
comment: 'Level-Form als String (o = kein Feld, x = Feld, Zeilen durch \n getrennt)'
|
||||
},
|
||||
boardWidth: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 8
|
||||
allowNull: true, // Ändern zu true, da bereits existierende Datensätze vorhanden sind
|
||||
defaultValue: 6, // Standardwert für neue Level
|
||||
comment: 'Breite des Level-Boards'
|
||||
},
|
||||
boardHeight: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true, // Ändern zu true, da bereits existierende Datensätze vorhanden sind
|
||||
defaultValue: 6, // Standardwert für neue Level
|
||||
comment: 'Höhe des Level-Boards'
|
||||
},
|
||||
tileTypes: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: false,
|
||||
defaultValue: ['gem', 'star', 'heart', 'diamond', 'circle', 'square']
|
||||
allowNull: true, // Ändern zu true, da wir jetzt eine Verknüpfungstabelle haben
|
||||
comment: 'Legacy: Array der verfügbaren Tile-Typen (wird durch levelTileTypes ersetzt)'
|
||||
},
|
||||
moveLimit: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
allowNull: false,
|
||||
defaultValue: 20
|
||||
},
|
||||
timeLimit: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
comment: 'Zeitlimit in Sekunden (null = kein Limit)'
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
tableName: 'match3_levels',
|
||||
schema: 'match3',
|
||||
timestamps: true
|
||||
timestamps: true,
|
||||
underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format
|
||||
});
|
||||
|
||||
export default Level;
|
||||
export default Match3Level;
|
||||
|
||||
53
backend/models/match3/levelTileType.js
Normal file
53
backend/models/match3/levelTileType.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
const Match3LevelTileType = sequelize.define('Match3LevelTileType', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
levelId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'match3_levels',
|
||||
key: 'id'
|
||||
},
|
||||
comment: 'Referenz auf den Level'
|
||||
},
|
||||
tileTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'match3_tile_types',
|
||||
key: 'id'
|
||||
},
|
||||
comment: 'Referenz auf den Tile-Typ'
|
||||
},
|
||||
weight: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: 'Gewichtung für die Wahrscheinlichkeit, dass dieser Tile-Typ erscheint'
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: 'Ob dieser Tile-Typ in diesem Level aktiv ist'
|
||||
}
|
||||
}, {
|
||||
tableName: 'match3_level_tile_types',
|
||||
schema: 'match3',
|
||||
timestamps: true,
|
||||
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['level_id', 'tile_type_id'] // WICHTIG: Bei underscored: true müssen snake_case Namen verwendet werden
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default Match3LevelTileType;
|
||||
@@ -46,7 +46,8 @@ const Objective = sequelize.define('Objective', {
|
||||
}, {
|
||||
tableName: 'match3_objectives',
|
||||
schema: 'match3',
|
||||
timestamps: true
|
||||
timestamps: true,
|
||||
underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format
|
||||
});
|
||||
|
||||
export default Objective;
|
||||
|
||||
61
backend/models/match3/tileType.js
Normal file
61
backend/models/match3/tileType.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
const Match3TileType = sequelize.define('Match3TileType', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: 'Eindeutiger Name des Tile-Typs (z.B. "gem", "star", "heart")'
|
||||
},
|
||||
displayName: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: 'Anzeigename des Tile-Typs (z.B. "Juwel", "Stern", "Herz")'
|
||||
},
|
||||
symbol: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: false,
|
||||
comment: 'Unicode-Symbol für den Tile-Typ (z.B. "💎", "⭐", "❤️")'
|
||||
},
|
||||
color: {
|
||||
type: DataTypes.STRING(7),
|
||||
allowNull: true,
|
||||
comment: 'Hex-Farbe für den Tile-Typ (z.B. "#ff6b6b")'
|
||||
},
|
||||
rarity: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'common',
|
||||
comment: 'Seltenheit des Tile-Typs (common, uncommon, rare, epic, legendary)'
|
||||
},
|
||||
points: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 10,
|
||||
comment: 'Punkte, die dieser Tile-Typ beim Matchen gibt'
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: 'Ob dieser Tile-Typ aktiv ist'
|
||||
}
|
||||
}, {
|
||||
tableName: 'match3_tile_types',
|
||||
schema: 'match3',
|
||||
timestamps: true,
|
||||
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['name']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default Match3TileType;
|
||||
@@ -67,10 +67,11 @@ const UserLevelProgress = sequelize.define('UserLevelProgress', {
|
||||
tableName: 'match3_user_level_progress',
|
||||
schema: 'match3',
|
||||
timestamps: true,
|
||||
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['userProgressId', 'levelId']
|
||||
fields: ['user_progress_id', 'level_id'] // WICHTIG: Bei underscored: true müssen snake_case Namen verwendet werden
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -51,10 +51,11 @@ const UserProgress = sequelize.define('UserProgress', {
|
||||
tableName: 'match3_user_progress',
|
||||
schema: 'match3',
|
||||
timestamps: true,
|
||||
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['userId', 'campaignId']
|
||||
fields: ['user_id', 'campaign_id'] // WICHTIG: Bei underscored: true müssen snake_case Namen verwendet werden
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -25,4 +25,29 @@ router.post('/falukant/searchuser', authenticate, adminController.searchUser);
|
||||
router.get('/falukant/getuser/:id', authenticate, adminController.getFalukantUserById);
|
||||
router.post('/falukant/edituser', authenticate, adminController.changeFalukantUser);
|
||||
|
||||
// --- Minigames Admin ---
|
||||
router.get('/minigames/match3/campaigns', authenticate, adminController.getMatch3Campaigns);
|
||||
router.get('/minigames/match3/campaigns/:id', authenticate, adminController.getMatch3Campaign);
|
||||
router.post('/minigames/match3/campaigns', authenticate, adminController.createMatch3Campaign);
|
||||
router.put('/minigames/match3/campaigns/:id', authenticate, adminController.updateMatch3Campaign);
|
||||
router.delete('/minigames/match3/campaigns/:id', authenticate, adminController.deleteMatch3Campaign);
|
||||
|
||||
router.get('/minigames/match3/levels', authenticate, adminController.getMatch3Levels);
|
||||
router.get('/minigames/match3/levels/:id', authenticate, adminController.getMatch3Level);
|
||||
router.post('/minigames/match3/levels', authenticate, adminController.createMatch3Level);
|
||||
router.put('/minigames/match3/levels/:id', authenticate, adminController.updateMatch3Level);
|
||||
router.delete('/minigames/match3/levels/:id', authenticate, adminController.deleteMatch3Level);
|
||||
|
||||
// Objectives
|
||||
router.get('/minigames/match3/objectives', authenticate, adminController.getMatch3Objectives);
|
||||
router.get('/minigames/match3/objectives/:id', authenticate, adminController.getMatch3Objective);
|
||||
router.post('/minigames/match3/objectives', authenticate, adminController.createMatch3Objective);
|
||||
router.put('/minigames/match3/objectives/:id', authenticate, adminController.updateMatch3Objective);
|
||||
router.delete('/minigames/match3/objectives/:id', authenticate, adminController.deleteMatch3Objective);
|
||||
|
||||
router.get('/minigames/match3/tile-types', authenticate, adminController.getMatch3TileTypes);
|
||||
router.post('/minigames/match3/tile-types', authenticate, adminController.createMatch3TileType);
|
||||
router.put('/minigames/match3/tile-types/:id', authenticate, adminController.updateMatch3TileType);
|
||||
router.delete('/minigames/match3/tile-types/:id', authenticate, adminController.deleteMatch3TileType);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import express from 'express';
|
||||
import match3Controller from '../controllers/match3Controller.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import Match3Controller from '../controllers/match3Controller.js';
|
||||
|
||||
const router = express.Router();
|
||||
const match3Controller = new Match3Controller();
|
||||
|
||||
// Alle Routen erfordern Authentifizierung
|
||||
// Alle Routen benötigen Authentifizierung
|
||||
router.use(authenticate);
|
||||
|
||||
// Kampagnen-Routen
|
||||
router.get('/campaigns', match3Controller.getCampaigns);
|
||||
router.get('/campaigns/:campaignId', match3Controller.getCampaign);
|
||||
router.get('/campaigns/:id', match3Controller.getCampaign);
|
||||
|
||||
// Benutzer-Fortschritt
|
||||
// Level-Routen
|
||||
router.get('/levels/:id', match3Controller.getLevel);
|
||||
|
||||
// Fortschritt-Routen
|
||||
router.get('/campaigns/:campaignId/progress', match3Controller.getUserProgress);
|
||||
router.post('/campaigns/:campaignId/levels/:levelId/progress', match3Controller.updateLevelProgress);
|
||||
router.post('/campaigns/:campaignId/cleanup', match3Controller.cleanupUserProgress);
|
||||
|
||||
// Bestenliste und Statistiken
|
||||
router.get('/campaigns/:campaignId/leaderboard', match3Controller.getLeaderboard);
|
||||
// Statistiken-Routen
|
||||
router.get('/stats', match3Controller.getUserStats);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -292,22 +292,34 @@ class AdminService {
|
||||
}
|
||||
|
||||
// --- Chat Room Admin ---
|
||||
async getRoomTypes() {
|
||||
async getRoomTypes(userId) {
|
||||
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
return await RoomType.findAll();
|
||||
}
|
||||
|
||||
async getGenderRestrictions() {
|
||||
async getGenderRestrictions(userId) {
|
||||
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
// Find the UserParamType for gender restriction (e.g. description = 'gender')
|
||||
const genderType = await UserParamType.findOne({ where: { description: 'gender' } });
|
||||
if (!genderType) return [];
|
||||
return await UserParamValue.findAll({ where: { userParamTypeId: genderType.id } });
|
||||
}
|
||||
|
||||
async getUserRights() {
|
||||
async getUserRights(userId) {
|
||||
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
return await ChatRight.findAll();
|
||||
}
|
||||
|
||||
async getRooms() {
|
||||
async getRooms(userId) {
|
||||
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
// Only return necessary fields to the frontend
|
||||
return await Room.findAll({
|
||||
attributes: [
|
||||
@@ -329,20 +341,301 @@ class AdminService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateRoom(id, data) {
|
||||
async updateRoom(userId, id, data) {
|
||||
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const room = await Room.findByPk(id);
|
||||
if (!room) throw new Error('Room not found');
|
||||
await room.update(data);
|
||||
return room;
|
||||
}
|
||||
|
||||
async createRoom(data) {
|
||||
async createRoom(userId, data) {
|
||||
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
return await Room.create(data);
|
||||
}
|
||||
|
||||
async deleteRoom(id) {
|
||||
async deleteRoom(userId, id) {
|
||||
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
return await Room.destroy({ where: { id } });
|
||||
}
|
||||
|
||||
// --- Match3 Admin Methods ---
|
||||
async getMatch3Campaigns(userId) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
||||
return await Match3Campaign.findAll({
|
||||
include: [{
|
||||
model: (await import('../models/match3/level.js')).default,
|
||||
as: 'levels',
|
||||
include: [{
|
||||
model: (await import('../models/match3/objective.js')).default,
|
||||
as: 'objectives',
|
||||
required: false
|
||||
}],
|
||||
required: false
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async getMatch3Campaign(userId, id) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
||||
return await Match3Campaign.findByPk(id, {
|
||||
include: [{
|
||||
model: (await import('../models/match3/level.js')).default,
|
||||
as: 'levels',
|
||||
include: [{
|
||||
model: (await import('../models/match3/objective.js')).default,
|
||||
as: 'objectives',
|
||||
required: false
|
||||
}],
|
||||
required: false
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async createMatch3Campaign(userId, data) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
||||
return await Match3Campaign.create(data);
|
||||
}
|
||||
|
||||
async updateMatch3Campaign(userId, id, data) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
||||
const campaign = await Match3Campaign.findByPk(id);
|
||||
if (!campaign) throw new Error('Campaign not found');
|
||||
await campaign.update(data);
|
||||
return campaign;
|
||||
}
|
||||
|
||||
async deleteMatch3Campaign(userId, id) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
||||
return await Match3Campaign.destroy({ where: { id } });
|
||||
}
|
||||
|
||||
async getMatch3Levels(userId) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Level = (await import('../models/match3/level.js')).default;
|
||||
return await Match3Level.findAll({
|
||||
include: [
|
||||
{
|
||||
model: (await import('../models/match3/campaign.js')).default,
|
||||
as: 'campaign',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: (await import('../models/match3/objective.js')).default,
|
||||
as: 'objectives',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
order: [['order', 'ASC']]
|
||||
});
|
||||
}
|
||||
|
||||
async getMatch3Level(userId, id) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Level = (await import('../models/match3/level.js')).default;
|
||||
return await Match3Level.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: (await import('../models/match3/campaign.js')).default,
|
||||
as: 'campaign',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: (await import('../models/match3/objective.js')).default,
|
||||
as: 'objectives',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async createMatch3Level(userId, data) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Level = (await import('../models/match3/level.js')).default;
|
||||
|
||||
// Wenn keine campaignId gesetzt ist, setze eine Standard-Campaign-ID
|
||||
if (!data.campaignId) {
|
||||
// Versuche eine Standard-Campaign zu finden oder erstelle eine
|
||||
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
||||
let defaultCampaign = await Match3Campaign.findOne({ where: { isActive: true } });
|
||||
|
||||
if (!defaultCampaign) {
|
||||
// Erstelle eine Standard-Campaign falls keine existiert
|
||||
defaultCampaign = await Match3Campaign.create({
|
||||
name: 'Standard Campaign',
|
||||
description: 'Standard Campaign für Match3 Levels',
|
||||
isActive: true,
|
||||
order: 1
|
||||
});
|
||||
}
|
||||
|
||||
data.campaignId = defaultCampaign.id;
|
||||
}
|
||||
|
||||
// Validiere, dass campaignId gesetzt ist
|
||||
if (!data.campaignId) {
|
||||
throw new Error('CampaignId ist erforderlich');
|
||||
}
|
||||
|
||||
return await Match3Level.create(data);
|
||||
}
|
||||
|
||||
async updateMatch3Level(userId, id, data) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Level = (await import('../models/match3/level.js')).default;
|
||||
const level = await Match3Level.findByPk(id);
|
||||
if (!level) throw new Error('Level not found');
|
||||
await level.update(data);
|
||||
return level;
|
||||
}
|
||||
|
||||
async deleteMatch3Level(userId, id) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Level = (await import('../models/match3/level.js')).default;
|
||||
return await Match3Level.destroy({ where: { id } });
|
||||
}
|
||||
|
||||
// Match3 Objectives
|
||||
async getMatch3Objectives(userId) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Objective = (await import('../models/match3/objective.js')).default;
|
||||
return await Match3Objective.findAll({
|
||||
include: [{
|
||||
model: (await import('../models/match3/level.js')).default,
|
||||
as: 'level',
|
||||
required: false
|
||||
}],
|
||||
order: [['order', 'ASC']]
|
||||
});
|
||||
}
|
||||
|
||||
async getMatch3Objective(userId, id) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Objective = (await import('../models/match3/objective.js')).default;
|
||||
return await Match3Objective.findByPk(id, {
|
||||
include: [{
|
||||
model: (await import('../models/match3/level.js')).default,
|
||||
as: 'level',
|
||||
required: false
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async createMatch3Objective(userId, data) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Objective = (await import('../models/match3/objective.js')).default;
|
||||
|
||||
// Validiere, dass levelId gesetzt ist
|
||||
if (!data.levelId) {
|
||||
throw new Error('LevelId ist erforderlich');
|
||||
}
|
||||
|
||||
// Validiere, dass target eine ganze Zahl ist
|
||||
if (data.target && !Number.isInteger(Number(data.target))) {
|
||||
throw new Error('Target muss eine ganze Zahl sein');
|
||||
}
|
||||
|
||||
return await Match3Objective.create(data);
|
||||
}
|
||||
|
||||
async updateMatch3Objective(userId, id, data) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Objective = (await import('../models/match3/objective.js')).default;
|
||||
const objective = await Match3Objective.findByPk(id);
|
||||
if (!objective) throw new Error('Objective not found');
|
||||
|
||||
// Validiere, dass target eine ganze Zahl ist
|
||||
if (data.target && !Number.isInteger(Number(data.target))) {
|
||||
throw new Error('Target muss eine ganze Zahl sein');
|
||||
}
|
||||
|
||||
await objective.update(data);
|
||||
return objective;
|
||||
}
|
||||
|
||||
async deleteMatch3Objective(userId, id) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3Objective = (await import('../models/match3/objective.js')).default;
|
||||
return await Match3Objective.destroy({ where: { id } });
|
||||
}
|
||||
|
||||
async getMatch3TileTypes(userId) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3TileType = (await import('../models/match3/tileType.js')).default;
|
||||
return await Match3TileType.findAll({
|
||||
order: [['name', 'ASC']]
|
||||
});
|
||||
}
|
||||
|
||||
async createMatch3TileType(userId, data) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3TileType = (await import('../models/match3/tileType.js')).default;
|
||||
return await Match3TileType.create(data);
|
||||
}
|
||||
|
||||
async updateMatch3TileType(userId, id, data) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3TileType = (await import('../models/match3/tileType.js')).default;
|
||||
const tileType = await Match3TileType.findByPk(id);
|
||||
if (!tileType) throw new Error('Tile type not found');
|
||||
await tileType.update(data);
|
||||
return tileType;
|
||||
}
|
||||
|
||||
async deleteMatch3TileType(userId, id) {
|
||||
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
const Match3TileType = (await import('../models/match3/tileType.js')).default;
|
||||
return await Match3TileType.destroy({ where: { id } });
|
||||
}
|
||||
}
|
||||
|
||||
export default new AdminService();
|
||||
@@ -3,311 +3,501 @@ import Match3Level from '../models/match3/level.js';
|
||||
import Match3Objective from '../models/match3/objective.js';
|
||||
import Match3UserProgress from '../models/match3/userProgress.js';
|
||||
import Match3UserLevelProgress from '../models/match3/userLevelProgress.js';
|
||||
import Match3TileType from '../models/match3/tileType.js';
|
||||
import Match3LevelTileType from '../models/match3/levelTileType.js';
|
||||
|
||||
class Match3Service {
|
||||
/**
|
||||
* Lädt alle aktiven Kampagnen
|
||||
*/
|
||||
async getActiveCampaigns() {
|
||||
try {
|
||||
const campaigns = await Match3Campaign.findAll({
|
||||
where: { isActive: true },
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'levels',
|
||||
// Lade alle aktiven Kampagnen
|
||||
async getCampaigns() {
|
||||
const campaigns = await Match3Campaign.findAll({
|
||||
where: { isActive: true },
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: Match3Objective,
|
||||
as: 'objectives',
|
||||
required: false,
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
],
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
],
|
||||
order: [['order', 'ASC']]
|
||||
});
|
||||
|
||||
return campaigns;
|
||||
} catch (error) {
|
||||
console.error('Error loading active campaigns:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt eine spezifische Kampagne mit allen Leveln
|
||||
*/
|
||||
async getCampaign(campaignId) {
|
||||
try {
|
||||
const campaign = await Match3Campaign.findByPk(campaignId, {
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'levels',
|
||||
where: { isActive: true },
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: Match3Objective,
|
||||
as: 'objectives',
|
||||
required: false,
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
],
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return campaign;
|
||||
} catch (error) {
|
||||
console.error('Error loading campaign:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt den Benutzerfortschritt für eine Kampagne
|
||||
*/
|
||||
async getUserProgress(userId, campaignId) {
|
||||
try {
|
||||
let userProgress = await Match3UserProgress.findOne({
|
||||
where: { userId, campaignId },
|
||||
include: [
|
||||
{
|
||||
model: Match3UserLevelProgress,
|
||||
as: 'levelProgress',
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'level'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!userProgress) {
|
||||
// Erstelle neuen Fortschritt wenn noch nicht vorhanden
|
||||
userProgress = await Match3UserProgress.create({
|
||||
userId,
|
||||
campaignId,
|
||||
totalScore: 0,
|
||||
totalStars: 0,
|
||||
levelsCompleted: 0,
|
||||
currentLevel: 1,
|
||||
isCompleted: false
|
||||
});
|
||||
} else {
|
||||
// Validiere und korrigiere bestehende currentLevel-Werte
|
||||
if (userProgress.currentLevel < 1 || userProgress.currentLevel > 1000) {
|
||||
console.warn(`Invalid currentLevel detected for user ${userId}: ${userProgress.currentLevel}, correcting to ${userProgress.levelsCompleted + 1}`);
|
||||
|
||||
// Korrigiere den ungültigen Wert
|
||||
await userProgress.update({
|
||||
currentLevel: userProgress.levelsCompleted + 1
|
||||
});
|
||||
|
||||
// Lade den aktualisierten Datensatz
|
||||
userProgress = await Match3UserProgress.findByPk(userProgress.id, {
|
||||
include: [
|
||||
{
|
||||
model: Match3UserLevelProgress,
|
||||
as: 'levelProgress',
|
||||
include: [
|
||||
{
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'level'
|
||||
}
|
||||
]
|
||||
}
|
||||
as: 'levels',
|
||||
where: { isActive: true },
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: Match3Objective,
|
||||
as: 'objectives',
|
||||
where: { isRequired: true },
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
order: [
|
||||
['id', 'ASC'],
|
||||
[{ model: Match3Level, as: 'levels' }, 'order', 'ASC'],
|
||||
[{ model: Match3Level, as: 'levels' }, { model: Match3Objective, as: 'objectives' }, 'order', 'ASC']
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
return campaigns;
|
||||
}
|
||||
|
||||
// Lade eine spezifische Kampagne
|
||||
async getCampaign(campaignId) {
|
||||
try {
|
||||
// Lade zuerst die Kampagne ohne levelTileTypes
|
||||
const campaign = await Match3Campaign.findByPk(campaignId, {
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'levels',
|
||||
where: { isActive: true },
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: Match3Objective,
|
||||
as: 'objectives',
|
||||
where: { isRequired: true },
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
order: [
|
||||
[{ model: Match3Level, as: 'levels' }, 'order', 'ASC'],
|
||||
[{ model: Match3Level, as: 'levels' }, { model: Match3Objective, as: 'objectives' }, 'order', 'ASC']
|
||||
]
|
||||
});
|
||||
|
||||
if (!campaign) {
|
||||
throw { status: 404, message: 'Campaign not found' };
|
||||
}
|
||||
|
||||
// Versuche levelTileTypes zu laden, aber mache es optional
|
||||
try {
|
||||
const levelsWithTileTypes = await Promise.all(
|
||||
campaign.levels.map(async (level) => {
|
||||
try {
|
||||
const levelTileTypes = await Match3LevelTileType.findAll({
|
||||
where: {
|
||||
levelId: level.id,
|
||||
isActive: true
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Match3TileType,
|
||||
as: 'tileType',
|
||||
where: { isActive: true },
|
||||
required: true
|
||||
}
|
||||
],
|
||||
order: [['weight', 'DESC']]
|
||||
});
|
||||
|
||||
return {
|
||||
...level.toJSON(),
|
||||
levelTileTypes: levelTileTypes
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(`Warnung: Konnte levelTileTypes für Level ${level.id} nicht laden:`, error.message);
|
||||
// Fallback: Verwende die alten tileTypes
|
||||
return {
|
||||
...level.toJSON(),
|
||||
levelTileTypes: []
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
campaign.levels = levelsWithTileTypes;
|
||||
} catch (error) {
|
||||
console.log('Warnung: Konnte levelTileTypes nicht laden, verwende Fallback:', error.message);
|
||||
// Fallback: Verwende die alten tileTypes
|
||||
campaign.levels = campaign.levels.map(level => ({
|
||||
...level.toJSON(),
|
||||
levelTileTypes: []
|
||||
}));
|
||||
}
|
||||
|
||||
return campaign;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Kampagne:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Lade ein spezifisches Level
|
||||
async getLevel(levelId) {
|
||||
const level = await Match3Level.findByPk(levelId, {
|
||||
include: [
|
||||
{
|
||||
model: Match3Objective,
|
||||
as: 'objectives',
|
||||
where: { isRequired: true },
|
||||
required: false,
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!level) {
|
||||
throw { status: 404, message: 'Level not found' };
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
// Lade Benutzer-Fortschritt für eine Kampagne
|
||||
async getUserProgress(userId, campaignId) {
|
||||
let userProgress = await Match3UserProgress.findOne({
|
||||
where: { userId, campaignId },
|
||||
include: [
|
||||
{
|
||||
model: Match3UserLevelProgress,
|
||||
as: 'levelProgress',
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'level'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!userProgress) {
|
||||
// Erstelle neuen Fortschritt wenn keiner existiert
|
||||
userProgress = await Match3UserProgress.create({
|
||||
userId,
|
||||
campaignId,
|
||||
totalScore: 0,
|
||||
totalStars: 0,
|
||||
levelsCompleted: 0,
|
||||
currentLevel: 1,
|
||||
isCompleted: false
|
||||
});
|
||||
}
|
||||
|
||||
// Validiere und korrigiere currentLevel falls nötig
|
||||
if (userProgress.currentLevel < 1 || userProgress.currentLevel > 1000) {
|
||||
const correctLevel = userProgress.levelsCompleted + 1;
|
||||
await userProgress.update({ currentLevel: correctLevel });
|
||||
}
|
||||
|
||||
return userProgress;
|
||||
}
|
||||
|
||||
// Aktualisiere Level-Fortschritt
|
||||
async updateLevelProgress(userId, campaignId, levelId, score, moves, time, stars, securityHash, timestamp) {
|
||||
// ANTI-CHEAT-VALIDIERUNG
|
||||
if (!this.validateProgressHash(userId, campaignId, levelId, score, moves, time, stars, securityHash, timestamp)) {
|
||||
throw { status: 403, message: 'Progress validation failed - possible cheating detected' };
|
||||
}
|
||||
|
||||
return userProgress;
|
||||
} catch (error) {
|
||||
console.error('Error loading user progress:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Level-Fortschritt eines Benutzers
|
||||
*/
|
||||
async updateLevelProgress(userId, campaignId, levelId, levelData) {
|
||||
try {
|
||||
// Lade oder erstelle Benutzerfortschritt
|
||||
// Lade oder erstelle Benutzer-Fortschritt
|
||||
let userProgress = await Match3UserProgress.findOne({
|
||||
where: { userId, campaignId }
|
||||
});
|
||||
|
||||
if (!userProgress) {
|
||||
userProgress = await Match3UserProgress.create({
|
||||
userId,
|
||||
campaignId,
|
||||
totalScore: 0,
|
||||
totalStars: 0,
|
||||
levelsCompleted: 0,
|
||||
currentLevel: 1,
|
||||
isCompleted: false
|
||||
});
|
||||
}
|
||||
if (!userProgress) {
|
||||
userProgress = await Match3UserProgress.create({
|
||||
userId,
|
||||
campaignId,
|
||||
totalScore: 0,
|
||||
totalStars: 0,
|
||||
levelsCompleted: 0,
|
||||
currentLevel: 1,
|
||||
isCompleted: false
|
||||
});
|
||||
}
|
||||
|
||||
// Lade oder erstelle Level-Fortschritt
|
||||
let levelProgress = await Match3UserLevelProgress.findOne({
|
||||
where: { userProgressId: userProgress.id, levelId }
|
||||
});
|
||||
|
||||
if (!levelProgress) {
|
||||
levelProgress = await Match3UserLevelProgress.create({
|
||||
userProgressId: userProgress.id,
|
||||
levelId,
|
||||
score: 0,
|
||||
moves: 0,
|
||||
time: 0,
|
||||
stars: 0,
|
||||
isCompleted: false,
|
||||
attempts: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Aktualisiere Level-Fortschritt
|
||||
const updateData = {
|
||||
score: Math.max(levelProgress.bestScore, levelData.score),
|
||||
moves: levelData.moves,
|
||||
time: levelData.time || 0,
|
||||
stars: Math.max(levelProgress.stars, levelData.stars),
|
||||
isCompleted: levelData.isCompleted || false,
|
||||
attempts: levelProgress.attempts + 1
|
||||
};
|
||||
|
||||
if (levelData.isCompleted) {
|
||||
updateData.completedAt = new Date();
|
||||
}
|
||||
|
||||
await levelProgress.update(updateData);
|
||||
|
||||
// Aktualisiere Bestwerte
|
||||
if (levelData.score > levelProgress.bestScore) {
|
||||
await levelProgress.update({ bestScore: levelData.score });
|
||||
}
|
||||
if (levelData.moves < levelProgress.bestMoves || levelProgress.bestMoves === 0) {
|
||||
await levelProgress.update({ bestMoves: levelData.moves });
|
||||
}
|
||||
if (levelData.time < levelProgress.bestTime || levelProgress.bestTime === 0) {
|
||||
await levelProgress.update({ bestTime: levelData.time });
|
||||
}
|
||||
|
||||
// Aktualisiere Kampagnen-Fortschritt
|
||||
if (levelData.isCompleted) {
|
||||
const totalScore = await Match3UserLevelProgress.sum('score', {
|
||||
where: { userProgressId: userProgress.id, isCompleted: true }
|
||||
// Lade oder erstelle Level-Fortschritt
|
||||
let levelProgress = await Match3UserLevelProgress.findOne({
|
||||
where: { userProgressId: userProgress.id, levelId }
|
||||
});
|
||||
|
||||
const totalStars = await Match3UserLevelProgress.sum('stars', {
|
||||
where: { userProgressId: userProgress.id, isCompleted: true }
|
||||
if (!levelProgress) {
|
||||
levelProgress = await Match3UserLevelProgress.create({
|
||||
userProgressId: userProgress.id,
|
||||
levelId,
|
||||
score: 0,
|
||||
moves: 0,
|
||||
time: 0,
|
||||
stars: 0,
|
||||
isCompleted: false,
|
||||
attempts: 0,
|
||||
bestScore: 0,
|
||||
bestMoves: 0,
|
||||
bestTime: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Aktualisiere Level-Fortschritt
|
||||
// WICHTIG: Ein Level ist nur abgeschlossen, wenn es mindestens 1 Stern gibt UND der Score > 0 ist
|
||||
// Das verhindert, dass unvollständige Level als abgeschlossen markiert werden
|
||||
const isCompleted = stars > 0 && score > 0;
|
||||
const attempts = levelProgress.attempts + 1;
|
||||
|
||||
await levelProgress.update({
|
||||
score: Math.max(score, levelProgress.bestScore),
|
||||
moves: moves,
|
||||
time: time,
|
||||
stars: Math.max(stars, levelProgress.stars),
|
||||
// WICHTIG: isCompleted wird NUR auf true gesetzt, wenn das Level tatsächlich abgeschlossen ist
|
||||
// Der alte Status wird NICHT beibehalten, da das zu falschen Abschlüssen führen kann
|
||||
isCompleted: isCompleted,
|
||||
attempts: attempts,
|
||||
bestScore: Math.max(score, levelProgress.bestScore),
|
||||
bestMoves: Math.min(moves, levelProgress.bestMoves || moves),
|
||||
bestTime: Math.min(time, levelProgress.bestTime || time),
|
||||
completedAt: isCompleted ? new Date() : levelProgress.completedAt
|
||||
});
|
||||
|
||||
// Aktualisiere Gesamt-Fortschritt
|
||||
// WICHTIG: Nur wenn das Level abgeschlossen ist, werden Score und Stars zum Gesamtfortschritt hinzugefügt
|
||||
// Das verhindert, dass unvollständige Level den Gesamtfortschritt beeinflussen
|
||||
let totalScore = userProgress.totalScore;
|
||||
let totalStars = userProgress.totalStars;
|
||||
|
||||
if (isCompleted) {
|
||||
totalScore += score;
|
||||
totalStars += stars;
|
||||
}
|
||||
|
||||
// Berechne neue currentLevel
|
||||
// WICHTIG: Zähle nur Level, die tatsächlich abgeschlossen sind
|
||||
const levelsCompleted = await Match3UserLevelProgress.count({
|
||||
where: { userProgressId: userProgress.id, isCompleted: true }
|
||||
where: {
|
||||
userProgressId: userProgress.id,
|
||||
isCompleted: true
|
||||
}
|
||||
});
|
||||
|
||||
// Korrigiere currentLevel: Es sollte immer levelsCompleted + 1 sein
|
||||
const correctCurrentLevel = levelsCompleted + 1;
|
||||
// Zusätzliche Sicherheit: Stelle sicher, dass levelsCompleted nicht größer als die verfügbaren Level ist
|
||||
if (levelsCompleted > 10) { // Angenommen, es gibt maximal 10 Level
|
||||
console.warn(`User ${userId} hat ungewöhnlich viele abgeschlossene Level: ${levelsCompleted}`);
|
||||
}
|
||||
|
||||
// WICHTIG: currentLevel sollte nur aktualisiert werden, wenn das Level tatsächlich abgeschlossen ist
|
||||
// Wenn das Level nicht abgeschlossen ist, behalte den aktuellen currentLevel bei
|
||||
let newCurrentLevel = userProgress.currentLevel;
|
||||
|
||||
if (isCompleted) {
|
||||
// Nur wenn das Level abgeschlossen ist, setze currentLevel auf das nächste Level
|
||||
newCurrentLevel = levelsCompleted + 1;
|
||||
}
|
||||
|
||||
await userProgress.update({
|
||||
totalScore,
|
||||
totalStars,
|
||||
levelsCompleted,
|
||||
currentLevel: newCurrentLevel,
|
||||
lastPlayed: new Date()
|
||||
});
|
||||
|
||||
return {
|
||||
levelProgress,
|
||||
userProgress: {
|
||||
totalScore,
|
||||
totalStars,
|
||||
levelsCompleted,
|
||||
currentLevel: newCurrentLevel
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Bereinige falsche Level-Abschlüsse für einen Benutzer
|
||||
async cleanupUserProgress(userId, campaignId) {
|
||||
try {
|
||||
// Lade Benutzer-Fortschritt
|
||||
const userProgress = await Match3UserProgress.findOne({
|
||||
where: { userId, campaignId }
|
||||
});
|
||||
|
||||
if (!userProgress) {
|
||||
return { success: false, message: 'User progress not found' };
|
||||
}
|
||||
|
||||
// Lade alle Level-Fortschritte
|
||||
const levelProgresses = await Match3UserLevelProgress.findAll({
|
||||
where: { userProgressId: userProgress.id }
|
||||
});
|
||||
|
||||
let cleanedCount = 0;
|
||||
let totalScore = 0;
|
||||
let totalStars = 0;
|
||||
let levelsCompleted = 0;
|
||||
|
||||
// Bereinige jeden Level-Fortschritt
|
||||
for (const levelProgress of levelProgresses) {
|
||||
// Ein Level ist nur abgeschlossen, wenn es mindestens 1 Stern UND Score > 0 hat
|
||||
const shouldBeCompleted = levelProgress.stars > 0 && levelProgress.score > 0;
|
||||
|
||||
if (levelProgress.isCompleted !== shouldBeCompleted) {
|
||||
await levelProgress.update({
|
||||
isCompleted: shouldBeCompleted,
|
||||
completedAt: shouldBeCompleted ? levelProgress.completedAt : null
|
||||
});
|
||||
cleanedCount++;
|
||||
}
|
||||
|
||||
// Sammle korrekte Statistiken
|
||||
if (shouldBeCompleted) {
|
||||
totalScore += levelProgress.score;
|
||||
totalStars += levelProgress.stars;
|
||||
levelsCompleted++;
|
||||
}
|
||||
}
|
||||
|
||||
// Aktualisiere Gesamt-Fortschritt
|
||||
await userProgress.update({
|
||||
totalScore,
|
||||
totalStars,
|
||||
levelsCompleted,
|
||||
currentLevel: correctCurrentLevel, // Verwende den korrigierten Wert
|
||||
lastPlayed: new Date()
|
||||
currentLevel: levelsCompleted + 1
|
||||
});
|
||||
|
||||
// Prüfe ob Kampagne abgeschlossen ist
|
||||
const totalLevels = await Match3Level.count({
|
||||
where: { campaignId, isActive: true }
|
||||
});
|
||||
|
||||
if (levelsCompleted >= totalLevels) {
|
||||
await userProgress.update({ isCompleted: true });
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: `Cleaned ${cleanedCount} level progress entries`,
|
||||
cleanedCount,
|
||||
totalScore,
|
||||
totalStars,
|
||||
levelsCompleted,
|
||||
currentLevel: levelsCompleted + 1
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error cleaning user progress:', error);
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
|
||||
return { userProgress, levelProgress };
|
||||
} catch (error) {
|
||||
console.error('Error updating level progress:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Bestenliste für eine Kampagne
|
||||
*/
|
||||
async getLeaderboard(campaignId, limit = 10) {
|
||||
try {
|
||||
const leaderboard = await Match3UserProgress.findAll({
|
||||
where: { campaignId },
|
||||
include: [
|
||||
{
|
||||
model: Match3UserLevelProgress,
|
||||
as: 'levelProgress',
|
||||
where: { isCompleted: true },
|
||||
required: false
|
||||
// ANTI-CHEAT: Validiere den Progress-Hash
|
||||
validateProgressHash(userId, campaignId, levelId, score, moves, time, stars, securityHash, timestamp) {
|
||||
try {
|
||||
// Prüfe ob der Timestamp nicht zu alt ist (5 Minuten)
|
||||
const now = Date.now();
|
||||
if (now - timestamp > 5 * 60 * 1000) {
|
||||
console.warn(`Progress validation failed: Timestamp too old for user ${userId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Lade Level-Daten für Hash-Validierung
|
||||
return Match3Level.findByPk(levelId).then(level => {
|
||||
if (!level) {
|
||||
console.warn(`Progress validation failed: Level ${levelId} not found`);
|
||||
return false;
|
||||
}
|
||||
],
|
||||
order: [
|
||||
['totalScore', 'DESC'],
|
||||
['totalStars', 'DESC'],
|
||||
['levelsCompleted', 'DESC']
|
||||
],
|
||||
limit
|
||||
});
|
||||
|
||||
return leaderboard;
|
||||
} catch (error) {
|
||||
console.error('Error loading leaderboard:', error);
|
||||
throw error;
|
||||
// Erstelle den gleichen Hash wie im Frontend
|
||||
const dataString = `${levelId}|${score}|${moves}|${stars}|true|${level.boardLayout}|${level.moveLimit}`;
|
||||
|
||||
// Einfache Hash-Funktion (muss mit Frontend übereinstimmen)
|
||||
let hash = 0;
|
||||
for (let i = 0; i < dataString.length; i++) {
|
||||
const char = dataString.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
|
||||
// Salt hinzufügen (muss mit Frontend übereinstimmen)
|
||||
const salt = 'YourPart3_Match3_Security_2024';
|
||||
const saltedString = `${dataString}|${salt}`;
|
||||
|
||||
let saltedHash = 0;
|
||||
for (let i = 0; i < saltedString.length; i++) {
|
||||
const char = saltedString.charCodeAt(i);
|
||||
saltedHash = ((saltedHash << 5) - hash) + char;
|
||||
saltedHash = saltedHash & saltedHash;
|
||||
}
|
||||
|
||||
const expectedHash = Math.abs(saltedHash).toString(16);
|
||||
|
||||
// Vergleiche Hash
|
||||
if (expectedHash !== securityHash) {
|
||||
console.warn(`Progress validation failed: Hash mismatch for user ${userId}. Expected: ${expectedHash}, Got: ${securityHash}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Zusätzliche Validierungen
|
||||
if (score < 0 || moves < 0 || stars < 0 || stars > 3) {
|
||||
console.warn(`Progress validation failed: Invalid values for user ${userId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prüfe ob der Score realistisch ist (basierend auf Moves und Level)
|
||||
const maxPossibleScore = moves * 100 * levelId; // Vereinfachte Berechnung
|
||||
if (score > maxPossibleScore * 2) { // Erlaube 2x den maximalen Score
|
||||
console.warn(`Progress validation failed: Unrealistic score for user ${userId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Progress validation error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Statistiken für einen Benutzer
|
||||
*/
|
||||
async getUserStats(userId) {
|
||||
try {
|
||||
const stats = await Match3UserProgress.findAll({
|
||||
where: { userId },
|
||||
include: [
|
||||
{
|
||||
model: Match3Campaign,
|
||||
as: 'campaign'
|
||||
},
|
||||
{
|
||||
model: Match3UserLevelProgress,
|
||||
as: 'levelProgress',
|
||||
// Lade Benutzer-Statistiken
|
||||
async getUserStats(userId) {
|
||||
const userProgress = await Match3UserProgress.findAll({
|
||||
where: { userId },
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'level'
|
||||
}
|
||||
{
|
||||
model: Match3UserLevelProgress,
|
||||
as: 'levelProgress',
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'level'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('Error loading user stats:', error);
|
||||
throw error;
|
||||
if (!userProgress || userProgress.length === 0) {
|
||||
return {
|
||||
totalScore: 0,
|
||||
totalStars: 0,
|
||||
levelsCompleted: 0,
|
||||
totalPlayTime: 0,
|
||||
averageScore: 0,
|
||||
bestLevel: null
|
||||
};
|
||||
}
|
||||
|
||||
const totalScore = userProgress.reduce((sum, campaign) => sum + campaign.totalScore, 0);
|
||||
const totalStars = userProgress.reduce((sum, campaign) => sum + campaign.totalStars, 0);
|
||||
const levelsCompleted = userProgress.reduce((sum, campaign) => sum + campaign.levelsCompleted, 0);
|
||||
|
||||
let totalPlayTime = 0;
|
||||
let totalLevels = 0;
|
||||
let bestLevel = null;
|
||||
let bestScore = 0;
|
||||
|
||||
userProgress.forEach(campaign => {
|
||||
campaign.levelProgress.forEach(level => {
|
||||
if (level.time) {
|
||||
totalPlayTime += level.time;
|
||||
totalLevels++;
|
||||
}
|
||||
if (level.score > bestScore) {
|
||||
bestScore = level.score;
|
||||
bestLevel = level.level;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
totalScore,
|
||||
totalStars,
|
||||
levelsCompleted,
|
||||
totalPlayTime,
|
||||
averageScore: totalLevels > 0 ? Math.round(totalScore / totalLevels) : 0,
|
||||
bestLevel: bestLevel ? {
|
||||
name: bestLevel.name,
|
||||
score: bestScore
|
||||
} : null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default new Match3Service();
|
||||
|
||||
61
backend/utils/checkRightsTable.js
Normal file
61
backend/utils/checkRightsTable.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { sequelize } from './sequelize.js';
|
||||
|
||||
async function checkRightsTable() {
|
||||
try {
|
||||
console.log('🔍 Überprüfe den aktuellen Zustand der chat.rights Tabelle...');
|
||||
|
||||
// Überprüfe die Constraints der chat.rights Tabelle
|
||||
const rightsConstraints = await sequelize.query(`
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
tc.constraint_type,
|
||||
kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
LEFT JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.table_schema = 'chat'
|
||||
AND tc.table_name = 'rights'
|
||||
ORDER BY tc.constraint_type, kcu.column_name;
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
console.log(`📊 Chat Rights Constraints: ${rightsConstraints.length} gefunden`);
|
||||
rightsConstraints.forEach(constraint => {
|
||||
console.log(` - ${constraint.constraint_type} (${constraint.constraint_name}) auf Spalte: ${constraint.column_name}`);
|
||||
});
|
||||
|
||||
// Überprüfe speziell die UNIQUE Constraints auf der tr-Spalte
|
||||
const trUniqueConstraints = rightsConstraints.filter(c =>
|
||||
c.constraint_type === 'UNIQUE' && c.column_name === 'tr'
|
||||
);
|
||||
|
||||
console.log(`\n🎯 UNIQUE Constraints auf der tr-Spalte: ${trUniqueConstraints.length}`);
|
||||
if (trUniqueConstraints.length === 1) {
|
||||
console.log('✅ Perfekt! Es gibt nur noch einen UNIQUE Constraint auf der tr-Spalte.');
|
||||
} else if (trUniqueConstraints.length === 0) {
|
||||
console.log('⚠️ Es gibt keinen UNIQUE Constraint auf der tr-Spalte!');
|
||||
} else {
|
||||
console.log(`❌ Es gibt immer noch ${trUniqueConstraints.length} UNIQUE Constraints auf der tr-Spalte.`);
|
||||
}
|
||||
|
||||
console.log('\n✅ Überprüfung abgeschlossen');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei der Überprüfung:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Führe das Skript aus, wenn es direkt aufgerufen wird
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
checkRightsTable()
|
||||
.then(() => {
|
||||
console.log('🎯 Überprüfung der chat.rights Tabelle abgeschlossen');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Überprüfung fehlgeschlagen:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export default checkRightsTable;
|
||||
234
backend/utils/cleanupDatabaseConstraints.js
Normal file
234
backend/utils/cleanupDatabaseConstraints.js
Normal file
@@ -0,0 +1,234 @@
|
||||
import { sequelize } from './sequelize.js';
|
||||
|
||||
/**
|
||||
* Bereinigt doppelte Constraints und Indexe in der Datenbank
|
||||
* Dies sollte nur einmal ausgeführt werden, um bestehende Probleme zu beheben
|
||||
*/
|
||||
async function cleanupDatabaseConstraints() {
|
||||
try {
|
||||
console.log('🧹 Starte Bereinigung der Datenbank-Constraints...');
|
||||
|
||||
// 1. Doppelte UNIQUE Constraints entfernen
|
||||
console.log('🔍 Suche nach doppelten UNIQUE Constraints...');
|
||||
|
||||
const duplicateUniqueConstraints = await sequelize.query(`
|
||||
SELECT
|
||||
tc.table_schema,
|
||||
tc.table_name,
|
||||
kcu.column_name,
|
||||
COUNT(*) as constraint_count,
|
||||
array_agg(tc.constraint_name) as constraint_names
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.constraint_type = 'UNIQUE'
|
||||
AND tc.table_schema IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
|
||||
GROUP BY tc.table_schema, tc.table_name, kcu.column_name
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY tc.table_name, kcu.column_name;
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
console.log(`📊 Gefunden: ${duplicateUniqueConstraints.length} Spalten mit doppelten UNIQUE Constraints`);
|
||||
|
||||
// Entferne doppelte UNIQUE Constraints
|
||||
for (const duplicate of duplicateUniqueConstraints) {
|
||||
console.log(`🗑️ Entferne doppelte UNIQUE Constraints für ${duplicate.table_schema}.${duplicate.table_name}.${duplicate.column_name}`);
|
||||
|
||||
// Behalte den ersten Constraint, entferne die anderen
|
||||
const constraintNames = duplicate.constraint_names;
|
||||
for (let i = 1; i < constraintNames.length; i++) {
|
||||
const constraintName = constraintNames[i];
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE ${duplicate.table_schema}.${duplicate.table_name}
|
||||
DROP CONSTRAINT IF EXISTS "${constraintName}"
|
||||
`);
|
||||
console.log(` ✅ Entfernt: ${constraintName}`);
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Konnte nicht entfernen: ${constraintName} - ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Doppelte CHECK Constraints entfernen (korrigierte Abfrage)
|
||||
console.log('🔍 Suche nach doppelten CHECK Constraints...');
|
||||
|
||||
try {
|
||||
const duplicateCheckConstraints = await sequelize.query(`
|
||||
SELECT
|
||||
ttc.table_schema,
|
||||
ttc.table_name,
|
||||
tc.constraint_name,
|
||||
tc.check_clause
|
||||
FROM information_schema.check_constraints tc
|
||||
JOIN information_schema.table_constraints ttc
|
||||
ON tc.constraint_name = ttc.constraint_name
|
||||
WHERE ttc.table_schema IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
|
||||
ORDER BY ttc.table_name, tc.constraint_name;
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
console.log(`📊 Gefunden: ${duplicateCheckConstraints.length} CHECK Constraints`);
|
||||
} catch (error) {
|
||||
console.log(`⚠️ Konnte CHECK Constraints nicht abfragen: ${error.message}`);
|
||||
}
|
||||
|
||||
// 3. Doppelte Foreign Key Constraints entfernen
|
||||
console.log('🔍 Suche nach doppelten Foreign Key Constraints...');
|
||||
|
||||
const duplicateFKs = await sequelize.query(`
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
tc.table_name,
|
||||
tc.table_schema,
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
|
||||
ORDER BY tc.table_name, tc.constraint_name;
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
console.log(`📊 Gefunden: ${duplicateFKs.length} Foreign Key Constraints`);
|
||||
|
||||
// 4. Doppelte Indexe entfernen
|
||||
console.log('🔍 Suche nach doppelten Indexen...');
|
||||
|
||||
const duplicateIndexes = await sequelize.query(`
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
indexname,
|
||||
indexdef
|
||||
FROM pg_indexes
|
||||
WHERE schemaname IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
|
||||
AND indexname LIKE '%_index_%'
|
||||
ORDER BY tablename, indexname;
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
console.log(`📊 Gefunden: ${duplicateIndexes.length} potenziell doppelte Indexe`);
|
||||
|
||||
// 5. Spezifische Match3-Constraints prüfen
|
||||
console.log('🔍 Prüfe Match3-spezifische Constraints...');
|
||||
|
||||
const match3Constraints = await sequelize.query(`
|
||||
SELECT
|
||||
constraint_name,
|
||||
table_name,
|
||||
constraint_type
|
||||
FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'match3'
|
||||
ORDER BY table_name, constraint_type;
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
console.log(`📊 Match3 Constraints: ${match3Constraints.length} gefunden`);
|
||||
match3Constraints.forEach(constraint => {
|
||||
console.log(` - ${constraint.table_name}: ${constraint.constraint_type} (${constraint.constraint_name})`);
|
||||
});
|
||||
|
||||
// 6. Spezifische Chat-Constraints prüfen (da das Problem dort auftritt)
|
||||
console.log('🔍 Prüfe Chat-spezifische Constraints...');
|
||||
|
||||
try {
|
||||
const chatConstraints = await sequelize.query(`
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
tc.table_name,
|
||||
tc.constraint_type,
|
||||
kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
LEFT JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.table_schema = 'chat'
|
||||
ORDER BY tc.table_name, tc.constraint_type, kcu.column_name;
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
console.log(`📊 Chat Constraints: ${chatConstraints.length} gefunden`);
|
||||
chatConstraints.forEach(constraint => {
|
||||
console.log(` - ${constraint.table_name}: ${constraint.constraint_type} (${constraint.constraint_name}) auf Spalte: ${constraint.column_name}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`⚠️ Konnte Chat Constraints nicht abfragen: ${error.message}`);
|
||||
}
|
||||
|
||||
// 7. Spezifische Überprüfung der chat.rights Tabelle
|
||||
console.log('🔍 Spezielle Überprüfung der chat.rights Tabelle...');
|
||||
|
||||
try {
|
||||
const rightsConstraints = await sequelize.query(`
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
tc.constraint_type,
|
||||
kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
LEFT JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.table_schema = 'chat'
|
||||
AND tc.table_name = 'rights'
|
||||
ORDER BY tc.constraint_type, kcu.column_name;
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
console.log(`📊 Chat Rights Constraints: ${rightsConstraints.length} gefunden`);
|
||||
rightsConstraints.forEach(constraint => {
|
||||
console.log(` - ${constraint.constraint_type} (${constraint.constraint_name}) auf Spalte: ${constraint.column_name}`);
|
||||
});
|
||||
|
||||
// Entferne alle doppelten UNIQUE Constraints auf der tr-Spalte
|
||||
const trUniqueConstraints = rightsConstraints.filter(c =>
|
||||
c.constraint_type === 'UNIQUE' && c.column_name === 'tr'
|
||||
);
|
||||
|
||||
if (trUniqueConstraints.length > 1) {
|
||||
console.log(`🗑️ Entferne ${trUniqueConstraints.length - 1} doppelte UNIQUE Constraints auf chat.rights.tr`);
|
||||
|
||||
// Behalte den ersten, entferne die anderen
|
||||
for (let i = 1; i < trUniqueConstraints.length; i++) {
|
||||
const constraintName = trUniqueConstraints[i].constraint_name;
|
||||
try {
|
||||
await sequelize.query(`
|
||||
ALTER TABLE chat.rights
|
||||
DROP CONSTRAINT IF EXISTS "${constraintName}"
|
||||
`);
|
||||
console.log(` ✅ Entfernt: ${constraintName}`);
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Konnte nicht entfernen: ${constraintName} - ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`⚠️ Konnte chat.rights Constraints nicht abfragen: ${error.message}`);
|
||||
}
|
||||
|
||||
// 8. Empfehlungen ausgeben
|
||||
console.log('\n💡 Empfehlungen:');
|
||||
console.log('1. Überprüfe die oben gelisteten Constraints auf Duplikate');
|
||||
console.log('2. Verwende updateSchema() nur bei expliziten Schema-Änderungen');
|
||||
console.log('3. Normale syncModels() läuft jetzt ohne alter: true');
|
||||
console.log('4. Bei Problemen: Manuelle Bereinigung der doppelten Constraints');
|
||||
|
||||
console.log('✅ Datenbank-Constraint-Bereinigung abgeschlossen');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei der Constraint-Bereinigung:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Führe das Skript aus, wenn es direkt aufgerufen wird
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
cleanupDatabaseConstraints()
|
||||
.then(() => {
|
||||
console.log('🎯 Datenbank-Constraint-Bereinigung abgeschlossen');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Datenbank-Constraint-Bereinigung fehlgeschlagen:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export default cleanupDatabaseConstraints;
|
||||
@@ -1,56 +1,133 @@
|
||||
import { sequelize } from './sequelize.js';
|
||||
import Match3Campaign from '../models/match3/campaign.js';
|
||||
import Match3Level from '../models/match3/level.js';
|
||||
import Match3Objective from '../models/match3/objective.js';
|
||||
import Match3TileType from '../models/match3/tileType.js';
|
||||
import Match3LevelTileType from '../models/match3/levelTileType.js';
|
||||
|
||||
export const initializeMatch3Data = async () => {
|
||||
/**
|
||||
* Initialisiert die Match3-Daten in der Datenbank
|
||||
*/
|
||||
async function initializeMatch3Data() {
|
||||
try {
|
||||
console.log('🎯 Initialisiere Match3-Daten...');
|
||||
|
||||
// Prüfe ob bereits Daten vorhanden sind
|
||||
const existingCampaigns = await Match3Campaign.count();
|
||||
if (existingCampaigns > 0) {
|
||||
console.log('Match3 data already exists, skipping initialization');
|
||||
console.log('✅ Match3-Daten bereits vorhanden, überspringe Initialisierung');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Initializing Match3 data...');
|
||||
// Lösche existierende Level und erstelle sie neu
|
||||
console.log('🔄 Lösche existierende Level...');
|
||||
await Match3Level.destroy({ where: { campaignId: campaign.id } });
|
||||
console.log('✅ Existierende Level gelöscht');
|
||||
|
||||
// Erstelle erste Kampagne
|
||||
console.log('🎯 Erstelle neue Level...');
|
||||
|
||||
// Erstelle Kampagne
|
||||
const campaign = await Match3Campaign.create({
|
||||
name: 'Juwelen-Meister',
|
||||
description: 'Meistere die Kunst des Juwelen-Matchings',
|
||||
isActive: true,
|
||||
order: 1
|
||||
description: 'Meistere die Kunst des Juwelen-Matchings mit einzigartigen Level-Formen',
|
||||
isActive: true
|
||||
});
|
||||
|
||||
// Erstelle erste Level
|
||||
console.log('✅ Kampagne erstellt:', campaign.name);
|
||||
|
||||
// Erstelle Level 1: Einfaches 5x5 Feld
|
||||
const level1 = await Match3Level.create({
|
||||
campaignId: campaign.id,
|
||||
name: 'Der Anfang',
|
||||
description: 'Lerne die Grundlagen des Spiels',
|
||||
description: 'Lerne die Grundlagen mit einem einfachen 5x5 Feld',
|
||||
order: 1,
|
||||
boardSize: 6,
|
||||
boardLayout: 'xxxxx\nxxxxx\nxxxxx\nxxxxx\nxxxxx',
|
||||
boardWidth: 5,
|
||||
boardHeight: 5,
|
||||
tileTypes: ['gem', 'star', 'heart'],
|
||||
moveLimit: 15,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
// Erstelle Level 2: 7x6 Feld
|
||||
const level2 = await Match3Level.create({
|
||||
campaignId: campaign.id,
|
||||
name: 'Erste Herausforderung',
|
||||
description: 'Erweitere deine Fähigkeiten',
|
||||
description: 'Ein größeres 7x6 Feld stellt dich vor neue Herausforderungen',
|
||||
order: 2,
|
||||
boardSize: 7,
|
||||
boardLayout: 'xxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx',
|
||||
boardWidth: 7,
|
||||
boardHeight: 6,
|
||||
tileTypes: ['gem', 'star', 'heart', 'diamond'],
|
||||
moveLimit: 20,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
// Erstelle Level 3: L-Form mit festen Gems
|
||||
const level3 = await Match3Level.create({
|
||||
campaignId: campaign.id,
|
||||
name: 'Spielzug',
|
||||
description: 'Sei ein Profi',
|
||||
order: 3,
|
||||
boardLayout: 'xxxxx\nxooxx\nxxxgx\nxxxxx\nxxxgx',
|
||||
boardWidth: 5,
|
||||
boardHeight: 5,
|
||||
tileTypes: ['gem', 'star', 'heart', 'diamond'],
|
||||
moveLimit: 15,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
// Erstelle Level 4: H-Form
|
||||
const level4 = await Match3Level.create({
|
||||
campaignId: campaign.id,
|
||||
name: 'H-Form',
|
||||
description: 'Eine H-Form mit vielen Ecken und Kanten',
|
||||
order: 4,
|
||||
boardLayout: 'xxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx',
|
||||
boardWidth: 13,
|
||||
boardHeight: 13,
|
||||
tileTypes: ['gem', 'star', 'heart', 'diamond', 'crown', 'moon'],
|
||||
moveLimit: 30,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
// Erstelle Level 5: Diamant-Form
|
||||
const level5 = await Match3Level.create({
|
||||
campaignId: campaign.id,
|
||||
name: 'Diamant-Form',
|
||||
description: 'Eine elegante Diamant-Form für Fortgeschrittene',
|
||||
order: 5,
|
||||
boardLayout: 'oooxxxooo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\noooxxxooo',
|
||||
boardWidth: 9,
|
||||
boardHeight: 10,
|
||||
tileTypes: ['gem', 'star', 'heart', 'diamond', 'crown', 'moon', 'star'],
|
||||
moveLimit: 35,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
// Erstelle Level 6: Spiral-Form
|
||||
const level6 = await Match3Level.create({
|
||||
campaignId: campaign.id,
|
||||
name: 'Spiral-Form',
|
||||
description: 'Eine komplexe Spiral-Form für Meister',
|
||||
order: 6,
|
||||
boardLayout: 'xxxxxxxxxxxxx\nxooooooooooox\nxoxxxxxxxxxox\nxoxoooooooxox\nxoxoxxxxxoxox\nxoxoxoooxoxox\nxoxoxoxoxoxox\nxoxoxoooxoxox\nxoxoxxxxxoxox\nxoxoooooooxox\nxoxxxxxxxxxox\nxooooooooooox\nxxxxxxxxxxxxx',
|
||||
boardWidth: 13,
|
||||
boardHeight: 13,
|
||||
tileTypes: ['gem', 'star', 'heart', 'diamond', 'crown', 'moon', 'star', 'crystal'],
|
||||
moveLimit: 40,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
console.log('✅ Alle Level erstellt');
|
||||
|
||||
// Erstelle Objectives für Level 1
|
||||
await Match3Objective.bulkCreate([
|
||||
{
|
||||
levelId: level1.id,
|
||||
type: 'score',
|
||||
description: 'Sammle 100 Punkte',
|
||||
target: 100,
|
||||
description: 'Sammle 150 Punkte',
|
||||
target: 150,
|
||||
operator: '>=',
|
||||
order: 1,
|
||||
isRequired: true
|
||||
@@ -58,8 +135,8 @@ export const initializeMatch3Data = async () => {
|
||||
{
|
||||
levelId: level1.id,
|
||||
type: 'matches',
|
||||
description: 'Mache 3 Matches',
|
||||
target: 3,
|
||||
description: 'Mache 5 Matches',
|
||||
target: 5,
|
||||
operator: '>=',
|
||||
order: 2,
|
||||
isRequired: true
|
||||
@@ -71,8 +148,8 @@ export const initializeMatch3Data = async () => {
|
||||
{
|
||||
levelId: level2.id,
|
||||
type: 'score',
|
||||
description: 'Sammle 200 Punkte',
|
||||
target: 200,
|
||||
description: 'Sammle 250 Punkte',
|
||||
target: 250,
|
||||
operator: '>=',
|
||||
order: 1,
|
||||
isRequired: true
|
||||
@@ -80,8 +157,8 @@ export const initializeMatch3Data = async () => {
|
||||
{
|
||||
levelId: level2.id,
|
||||
type: 'matches',
|
||||
description: 'Mache 5 Matches',
|
||||
target: 5,
|
||||
description: 'Mache 8 Matches',
|
||||
target: 8,
|
||||
operator: '>=',
|
||||
order: 2,
|
||||
isRequired: true
|
||||
@@ -97,8 +174,137 @@ export const initializeMatch3Data = async () => {
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('Match3 data initialized successfully');
|
||||
// Erstelle Objectives für Level 3
|
||||
await Match3Objective.bulkCreate([
|
||||
{
|
||||
levelId: level3.id,
|
||||
type: 'score',
|
||||
description: 'Sammle 400 Punkte',
|
||||
target: 400,
|
||||
operator: '>=',
|
||||
order: 1,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level3.id,
|
||||
type: 'matches',
|
||||
description: 'Mache 12 Matches',
|
||||
target: 12,
|
||||
operator: '>=',
|
||||
order: 2,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level3.id,
|
||||
type: 'moves',
|
||||
description: 'Verwende weniger als 25 Züge',
|
||||
target: 25,
|
||||
operator: '<=',
|
||||
order: 3,
|
||||
isRequired: true
|
||||
}
|
||||
]);
|
||||
|
||||
// Erstelle Objectives für Level 4
|
||||
await Match3Objective.bulkCreate([
|
||||
{
|
||||
levelId: level4.id,
|
||||
type: 'score',
|
||||
description: 'Sammle 600 Punkte',
|
||||
target: 600,
|
||||
operator: '>=',
|
||||
order: 1,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level4.id,
|
||||
type: 'matches',
|
||||
description: 'Mache 15 Matches',
|
||||
target: 15,
|
||||
operator: '>=',
|
||||
order: 2,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level4.id,
|
||||
type: 'moves',
|
||||
description: 'Verwende weniger als 30 Züge',
|
||||
target: 30,
|
||||
operator: '<=',
|
||||
order: 3,
|
||||
isRequired: true
|
||||
}
|
||||
]);
|
||||
|
||||
// Erstelle Objectives für Level 5
|
||||
await Match3Objective.bulkCreate([
|
||||
{
|
||||
levelId: level5.id,
|
||||
type: 'score',
|
||||
description: 'Sammle 800 Punkte',
|
||||
target: 800,
|
||||
operator: '>=',
|
||||
order: 1,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level5.id,
|
||||
type: 'matches',
|
||||
description: 'Mache 18 Matches',
|
||||
target: 18,
|
||||
operator: '>=',
|
||||
order: 2,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level5.id,
|
||||
type: 'moves',
|
||||
description: 'Verwende weniger als 35 Züge',
|
||||
target: 35,
|
||||
operator: '<=',
|
||||
order: 3,
|
||||
isRequired: true
|
||||
}
|
||||
]);
|
||||
|
||||
// Erstelle Objectives für Level 6
|
||||
await Match3Objective.bulkCreate([
|
||||
{
|
||||
levelId: level6.id,
|
||||
type: 'score',
|
||||
description: 'Sammle 1000 Punkte',
|
||||
target: 1000,
|
||||
operator: '>=',
|
||||
order: 1,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level6.id,
|
||||
type: 'matches',
|
||||
description: 'Mache 25 Matches',
|
||||
target: 25,
|
||||
operator: '>=',
|
||||
order: 2,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level6.id,
|
||||
type: 'moves',
|
||||
description: 'Verwende weniger als 40 Züge',
|
||||
target: 40,
|
||||
operator: '<=',
|
||||
order: 3,
|
||||
isRequired: true
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ Alle Objectives erstellt');
|
||||
console.log('🎯 Match3-Daten erfolgreich initialisiert');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing Match3 data:', error);
|
||||
console.error('❌ Fehler beim Initialisieren der Match3-Daten:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default initializeMatch3Data;
|
||||
|
||||
119
backend/utils/initializeMatch3TileTypes.js
Normal file
119
backend/utils/initializeMatch3TileTypes.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { sequelize } from './sequelize.js';
|
||||
import Match3TileType from '../models/match3/tileType.js';
|
||||
|
||||
async function initializeMatch3TileTypes() {
|
||||
try {
|
||||
console.log('🚀 Initialisiere Match3 Tile-Typen...');
|
||||
|
||||
// Synchronisiere das TileType-Modell
|
||||
await Match3TileType.sync({ alter: true });
|
||||
console.log('✅ TileType-Modell synchronisiert');
|
||||
|
||||
// Standard-Tile-Typen definieren
|
||||
const defaultTileTypes = [
|
||||
{
|
||||
name: 'gem',
|
||||
displayName: 'Juwel',
|
||||
symbol: '💎',
|
||||
color: '#ff6b6b',
|
||||
rarity: 'common',
|
||||
points: 10
|
||||
},
|
||||
{
|
||||
name: 'star',
|
||||
displayName: 'Stern',
|
||||
symbol: '⭐',
|
||||
color: '#feca57',
|
||||
rarity: 'common',
|
||||
points: 15
|
||||
},
|
||||
{
|
||||
name: 'heart',
|
||||
displayName: 'Herz',
|
||||
symbol: '❤️',
|
||||
color: '#ff9ff3',
|
||||
rarity: 'common',
|
||||
points: 12
|
||||
},
|
||||
{
|
||||
name: 'diamond',
|
||||
displayName: 'Diamant',
|
||||
symbol: '🔷',
|
||||
color: '#54a0ff',
|
||||
rarity: 'uncommon',
|
||||
points: 20
|
||||
},
|
||||
{
|
||||
name: 'circle',
|
||||
displayName: 'Kreis',
|
||||
symbol: '⭕',
|
||||
color: '#5f27cd',
|
||||
rarity: 'uncommon',
|
||||
points: 18
|
||||
},
|
||||
{
|
||||
name: 'square',
|
||||
displayName: 'Quadrat',
|
||||
symbol: '🟦',
|
||||
color: '#00d2d3',
|
||||
rarity: 'rare',
|
||||
points: 25
|
||||
},
|
||||
{
|
||||
name: 'crown',
|
||||
displayName: 'Krone',
|
||||
symbol: '👑',
|
||||
color: '#ff9f43',
|
||||
rarity: 'epic',
|
||||
points: 35
|
||||
},
|
||||
{
|
||||
name: 'rainbow',
|
||||
displayName: 'Regenbogen',
|
||||
symbol: '🌈',
|
||||
color: '#ff6348',
|
||||
rarity: 'legendary',
|
||||
points: 50
|
||||
}
|
||||
];
|
||||
|
||||
// Tile-Typen erstellen oder aktualisieren
|
||||
for (const tileType of defaultTileTypes) {
|
||||
const [tileTypeInstance, created] = await Match3TileType.findOrCreate({
|
||||
where: { name: tileType.name },
|
||||
defaults: tileType
|
||||
});
|
||||
|
||||
if (created) {
|
||||
console.log(`✅ Tile-Typ "${tileType.displayName}" erstellt`);
|
||||
} else {
|
||||
// Aktualisiere bestehende Tile-Typen
|
||||
await Match3TileType.update(tileType, {
|
||||
where: { name: tileType.name }
|
||||
});
|
||||
console.log(`🔄 Tile-Typ "${tileType.displayName}" aktualisiert`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎉 Alle Match3 Tile-Typen erfolgreich initialisiert!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Initialisieren der Match3 Tile-Typen:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Führe die Initialisierung aus, wenn das Skript direkt aufgerufen wird
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
initializeMatch3TileTypes()
|
||||
.then(() => {
|
||||
console.log('✅ Tile-Typen-Initialisierung abgeschlossen');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler bei der Tile-Typen-Initialisierung:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export default initializeMatch3TileTypes;
|
||||
@@ -34,6 +34,10 @@ const initializeUserRights = async() => {
|
||||
where: { title: "developer"},
|
||||
defaults: { title: "developer"}
|
||||
});
|
||||
await UserRightType.findOrCreate({
|
||||
where: { title: "match3"},
|
||||
defaults: { title: "match3"}
|
||||
});
|
||||
};
|
||||
|
||||
export default initializeUserRights;
|
||||
|
||||
@@ -7,7 +7,9 @@ const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, proces
|
||||
host: process.env.DB_HOST,
|
||||
dialect: 'postgres',
|
||||
define: {
|
||||
timestamps: false
|
||||
timestamps: false,
|
||||
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
|
||||
freezeTableName: true // Verhindert Pluralisierung der Tabellennamen
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,9 +34,290 @@ const initializeDatabase = async () => {
|
||||
};
|
||||
|
||||
const syncModels = async (models) => {
|
||||
for (const model of Object.values(models)) {
|
||||
// Verwende force: false und alter: false, um Constraints nicht neu zu erstellen
|
||||
// Nur beim ersten Mal oder bei expliziten Schema-Änderungen sollte alter: true verwendet werden
|
||||
await model.sync({ alter: false, force: false });
|
||||
}
|
||||
};
|
||||
|
||||
// Intelligente Schema-Synchronisation - prüft ob Updates nötig sind
|
||||
const syncModelsWithUpdates = async (models) => {
|
||||
console.log('🔍 Prüfe ob Schema-Updates nötig sind...');
|
||||
|
||||
try {
|
||||
// Prüfe ob neue Felder existieren müssen
|
||||
const needsUpdate = await checkSchemaUpdates(models);
|
||||
|
||||
if (needsUpdate) {
|
||||
console.log('🔄 Schema-Updates nötig - verwende alter: true');
|
||||
for (const model of Object.values(models)) {
|
||||
await model.sync({ alter: true, force: false });
|
||||
}
|
||||
console.log('✅ Schema-Updates abgeschlossen');
|
||||
} else {
|
||||
console.log('✅ Keine Schema-Updates nötig - verwende alter: false');
|
||||
for (const model of Object.values(models)) {
|
||||
await model.sync({ alter: false, force: false });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei Schema-Synchronisation:', error);
|
||||
// Fallback: Normale Synchronisation ohne Updates
|
||||
console.log('🔄 Fallback: Normale Synchronisation ohne Updates');
|
||||
for (const model of Object.values(models)) {
|
||||
await model.sync({ alter: false, force: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Prüft ob Schema-Updates nötig sind
|
||||
const checkSchemaUpdates = async (models) => {
|
||||
try {
|
||||
console.log('🔍 Prüfe alle Schemas auf Updates...');
|
||||
|
||||
// Alle verfügbaren Schemas
|
||||
const schemas = [
|
||||
'community', 'logs', 'type', 'service', 'forum',
|
||||
'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log',
|
||||
'chat', 'match3'
|
||||
];
|
||||
|
||||
let needsUpdate = false;
|
||||
|
||||
// Prüfe jedes Schema
|
||||
for (const schema of schemas) {
|
||||
const schemaNeedsUpdate = await checkSchemaForUpdates(schema, models);
|
||||
if (schemaNeedsUpdate) {
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
return needsUpdate;
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei Schema-Prüfung:', error);
|
||||
return false; // Im Zweifelsfall: Keine Updates
|
||||
}
|
||||
};
|
||||
|
||||
// Prüft ein spezifisches Schema auf Updates
|
||||
const checkSchemaForUpdates = async (schemaName, models) => {
|
||||
try {
|
||||
console.log(`🔍 Prüfe Schema: ${schemaName}`);
|
||||
|
||||
// Hole alle Tabellen in diesem Schema
|
||||
const tables = await sequelize.query(`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = :schemaName
|
||||
ORDER BY table_name
|
||||
`, {
|
||||
replacements: { schemaName },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
if (tables.length === 0) {
|
||||
console.log(` 📊 Schema ${schemaName}: Keine Tabellen gefunden`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(` 📊 Schema ${schemaName}: ${tables.length} Tabellen gefunden`);
|
||||
|
||||
// Prüfe jede Tabelle auf Updates
|
||||
for (const table of tables) {
|
||||
const tableName = table.table_name;
|
||||
const tableNeedsUpdate = await checkTableForUpdates(schemaName, tableName, models);
|
||||
if (tableNeedsUpdate) {
|
||||
console.log(` 🔄 Tabelle ${tableName} braucht Updates`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe auf fehlende Tabellen (neue Models)
|
||||
const missingTables = await checkForMissingTables(schemaName, models);
|
||||
if (missingTables.length > 0) {
|
||||
console.log(` 🔄 Neue Tabellen gefunden: ${missingTables.join(', ')}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(` ✅ Schema ${schemaName}: Keine Updates nötig`);
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Fehler beim Prüfen von Schema ${schemaName}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Prüft auf fehlende Tabellen (neue Models)
|
||||
const checkForMissingTables = async (schemaName, models) => {
|
||||
try {
|
||||
const missingTables = [];
|
||||
|
||||
// Hole alle erwarteten Tabellen aus den Models
|
||||
for (const [modelName, model] of Object.entries(models)) {
|
||||
if (model._schema === schemaName) {
|
||||
const tableExists = await sequelize.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = :schemaName
|
||||
AND table_name = :tableName
|
||||
);
|
||||
`, {
|
||||
replacements: { schemaName, tableName: model.tableName },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
if (!tableExists[0]?.exists) {
|
||||
missingTables.push(model.tableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return missingTables;
|
||||
} catch (error) {
|
||||
console.error(`❌ Fehler beim Prüfen fehlender Tabellen:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Prüft eine spezifische Tabelle auf Updates
|
||||
const checkTableForUpdates = async (schemaName, tableName, models) => {
|
||||
try {
|
||||
// Finde das entsprechende Model
|
||||
const model = findModelForTable(schemaName, tableName, models);
|
||||
if (!model) {
|
||||
console.log(` ⚠️ Kein Model für Tabelle ${schemaName}.${tableName} gefunden`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hole aktuelle Spalten der Tabelle
|
||||
const currentColumns = await sequelize.query(`
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = :schemaName
|
||||
AND table_name = :tableName
|
||||
ORDER BY ordinal_position
|
||||
`, {
|
||||
replacements: { schemaName, tableName },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
// Hole erwartete Spalten aus dem Model
|
||||
const expectedColumns = Object.keys(model.rawAttributes);
|
||||
|
||||
// Vergleiche aktuelle und erwartete Spalten
|
||||
const missingColumns = expectedColumns.filter(expectedCol => {
|
||||
return !currentColumns.some(currentCol =>
|
||||
currentCol.column_name === expectedCol
|
||||
);
|
||||
});
|
||||
|
||||
if (missingColumns.length > 0) {
|
||||
console.log(` 📊 Fehlende Spalten in ${tableName}: ${missingColumns.join(', ')}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfe auf geänderte Spalten-Typen oder Constraints
|
||||
for (const expectedCol of expectedColumns) {
|
||||
const currentCol = currentColumns.find(col => col.column_name === expectedCol);
|
||||
if (currentCol) {
|
||||
const needsUpdate = await checkColumnForUpdates(
|
||||
schemaName, tableName, expectedCol, currentCol, model.rawAttributes[expectedCol]
|
||||
);
|
||||
if (needsUpdate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Fehler beim Prüfen von Tabelle ${schemaName}.${tableName}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Prüft eine spezifische Spalte auf Updates
|
||||
const checkColumnForUpdates = async (schemaName, tableName, columnName, currentColumn, expectedAttribute) => {
|
||||
try {
|
||||
// Prüfe Datentyp-Änderungen
|
||||
if (currentColumn.data_type !== getExpectedDataType(expectedAttribute)) {
|
||||
console.log(` 🔄 Spalte ${columnName}: Datentyp geändert (${currentColumn.data_type} → ${getExpectedDataType(expectedAttribute)})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfe NULL/NOT NULL Änderungen
|
||||
const currentNullable = currentColumn.is_nullable === 'YES';
|
||||
const expectedNullable = expectedAttribute.allowNull !== false;
|
||||
if (currentNullable !== expectedNullable) {
|
||||
console.log(` 🔄 Spalte ${columnName}: NULL-Constraint geändert (${currentNullable} → ${expectedNullable})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfe Standardwert-Änderungen
|
||||
if (expectedAttribute.defaultValue !== undefined &&
|
||||
currentColumn.column_default !== getExpectedDefaultValue(expectedAttribute.defaultValue)) {
|
||||
console.log(` 🔄 Spalte ${columnName}: Standardwert geändert`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Fehler beim Prüfen von Spalte ${columnName}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Hilfsfunktion: Findet Model für eine Tabelle
|
||||
const findModelForTable = (schemaName, tableName, models) => {
|
||||
// Suche nach dem Model basierend auf Schema und Tabellenname
|
||||
for (const [modelName, model] of Object.entries(models)) {
|
||||
if (model.tableName === tableName &&
|
||||
model._schema === schemaName) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Hilfsfunktion: Konvertiert Sequelize-Datentyp zu PostgreSQL-Datentyp
|
||||
const getExpectedDataType = (attribute) => {
|
||||
const type = attribute.type;
|
||||
|
||||
if (type instanceof sequelize.DataTypes.INTEGER) return 'integer';
|
||||
if (type instanceof sequelize.DataTypes.STRING) return 'character varying';
|
||||
if (type instanceof sequelize.DataTypes.TEXT) return 'text';
|
||||
if (type instanceof sequelize.DataTypes.BOOLEAN) return 'boolean';
|
||||
if (type instanceof sequelize.DataTypes.DATE) return 'timestamp without time zone';
|
||||
if (type instanceof sequelize.DataTypes.JSON) return 'json';
|
||||
if (type instanceof sequelize.DataTypes.DECIMAL) return 'numeric';
|
||||
|
||||
// Fallback
|
||||
return 'text';
|
||||
};
|
||||
|
||||
// Hilfsfunktion: Konvertiert Sequelize-Default zu PostgreSQL-Default
|
||||
const getExpectedDefaultValue = (defaultValue) => {
|
||||
if (defaultValue === null) return null;
|
||||
if (typeof defaultValue === 'string') return `'${defaultValue}'`;
|
||||
if (typeof defaultValue === 'number') return defaultValue.toString();
|
||||
if (typeof defaultValue === 'boolean') return defaultValue.toString();
|
||||
if (defaultValue === sequelize.literal('CURRENT_TIMESTAMP')) return 'CURRENT_TIMESTAMP';
|
||||
|
||||
// Fallback
|
||||
return defaultValue?.toString() || null;
|
||||
};
|
||||
|
||||
// Separate Funktion für Schema-Updates (nur bei Bedarf aufrufen)
|
||||
const updateSchema = async (models) => {
|
||||
console.log('🔄 Aktualisiere Datenbankschema...');
|
||||
for (const model of Object.values(models)) {
|
||||
await model.sync({ alter: true, force: false });
|
||||
}
|
||||
console.log('✅ Datenbankschema aktualisiert');
|
||||
};
|
||||
|
||||
async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, changedBy = null) {
|
||||
@@ -70,4 +353,4 @@ async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, ch
|
||||
}
|
||||
}
|
||||
|
||||
export { sequelize, initializeDatabase, syncModels, updateFalukantUserMoney };
|
||||
export { sequelize, initializeDatabase, syncModels, syncModelsWithUpdates, updateSchema, updateFalukantUserMoney };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// syncDatabase.js
|
||||
|
||||
import { initializeDatabase, syncModels } from './sequelize.js';
|
||||
import { initializeDatabase, syncModelsWithUpdates } from './sequelize.js';
|
||||
import initializeTypes from './initializeTypes.js';
|
||||
import initializeSettings from './initializeSettings.js';
|
||||
import initializeUserRights from './initializeUserRights.js';
|
||||
@@ -11,7 +11,8 @@ import models from '../models/index.js';
|
||||
import { createTriggers } from '../models/trigger.js';
|
||||
import initializeForum from './initializeForum.js';
|
||||
import initializeChat from './initializeChat.js';
|
||||
import { initializeMatch3Data } from './initializeMatch3.js';
|
||||
import initializeMatch3Data from './initializeMatch3.js';
|
||||
import updateExistingMatch3Levels from './updateExistingMatch3Levels.js';
|
||||
|
||||
const syncDatabase = async () => {
|
||||
try {
|
||||
@@ -19,7 +20,7 @@ const syncDatabase = async () => {
|
||||
await initializeDatabase();
|
||||
|
||||
console.log("Synchronizing models...");
|
||||
await syncModels(models);
|
||||
await syncModelsWithUpdates(models);
|
||||
|
||||
console.log("Setting up associations...");
|
||||
setupAssociations();
|
||||
@@ -48,6 +49,10 @@ const syncDatabase = async () => {
|
||||
console.log("Initializing chat...");
|
||||
await initializeChat();
|
||||
|
||||
// Match3-Initialisierung NACH der Model-Synchronisation
|
||||
console.log("Updating existing Match3 levels...");
|
||||
await updateExistingMatch3Levels();
|
||||
|
||||
console.log("Initializing Match3...");
|
||||
await initializeMatch3Data();
|
||||
|
||||
|
||||
74
backend/utils/updateExistingMatch3Levels.js
Normal file
74
backend/utils/updateExistingMatch3Levels.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { sequelize } from './sequelize.js';
|
||||
import Match3Level from '../models/match3/level.js';
|
||||
|
||||
/**
|
||||
* Aktualisiert existierende Match3-Level mit Standard-Layouts
|
||||
* und neuen Feldern
|
||||
*/
|
||||
async function updateExistingMatch3Levels() {
|
||||
try {
|
||||
console.log('🔧 Aktualisiere existierende Match3-Level...');
|
||||
|
||||
// Finde alle existierenden Level ohne boardLayout
|
||||
const existingLevels = await Match3Level.findAll({
|
||||
where: {
|
||||
boardLayout: null
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`📊 Gefunden: ${existingLevels.length} Level ohne boardLayout`);
|
||||
|
||||
if (existingLevels.length === 0) {
|
||||
console.log('✅ Alle Level haben bereits boardLayout');
|
||||
return;
|
||||
}
|
||||
|
||||
// Aktualisiere jeden Level mit Standard-Layout
|
||||
for (const level of existingLevels) {
|
||||
const oldBoardSize = level.boardSize || 6;
|
||||
|
||||
// Erstelle Standard-Layout basierend auf alter boardSize
|
||||
let boardLayout = '';
|
||||
for (let i = 0; i < oldBoardSize; i++) {
|
||||
for (let j = 0; j < oldBoardSize; j++) {
|
||||
boardLayout += 'x';
|
||||
}
|
||||
if (i < oldBoardSize - 1) boardLayout += '\n';
|
||||
}
|
||||
|
||||
// Aktualisiere den Level mit allen neuen Feldern
|
||||
await level.update({
|
||||
boardLayout: boardLayout,
|
||||
boardWidth: oldBoardSize,
|
||||
boardHeight: oldBoardSize,
|
||||
// Stelle sicher, dass alle erforderlichen Felder gesetzt sind
|
||||
tileTypes: level.tileTypes || ['gem', 'star', 'heart'],
|
||||
moveLimit: level.moveLimit || 20,
|
||||
isActive: level.isActive !== undefined ? level.isActive : true
|
||||
});
|
||||
|
||||
console.log(`🔧 Level ${level.id} aktualisiert: ${oldBoardSize}x${oldBoardSize} → alle neuen Felder gesetzt`);
|
||||
}
|
||||
|
||||
console.log('✅ Alle existierenden Level wurden aktualisiert');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Aktualisieren der Match3-Level:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Führe das Skript aus, wenn es direkt aufgerufen wird
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
updateExistingMatch3Levels()
|
||||
.then(() => {
|
||||
console.log('🎯 Match3-Level-Update abgeschlossen');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Match3-Level-Update fehlgeschlagen:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export default updateExistingMatch3Levels;
|
||||
115
backend/utils/updateMatch3LevelsWithTileTypes.js
Normal file
115
backend/utils/updateMatch3LevelsWithTileTypes.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import { sequelize } from './sequelize.js';
|
||||
import Match3Level from '../models/match3/level.js';
|
||||
import Match3TileType from '../models/match3/tileType.js';
|
||||
import Match3LevelTileType from '../models/match3/levelTileType.js';
|
||||
|
||||
/**
|
||||
* Aktualisiert bestehende Match3 Level mit den neuen Tile-Typen
|
||||
*/
|
||||
async function updateMatch3LevelsWithTileTypes() {
|
||||
try {
|
||||
console.log('🔄 Aktualisiere Match3 Level mit neuen Tile-Typen...');
|
||||
|
||||
// Synchronisiere die neuen Modelle
|
||||
await Match3TileType.sync({ alter: true });
|
||||
await Match3LevelTileType.sync({ alter: true });
|
||||
console.log('✅ Neue Modelle synchronisiert');
|
||||
|
||||
// Hole alle Level
|
||||
const levels = await Match3Level.findAll();
|
||||
console.log(`📊 ${levels.length} Level gefunden`);
|
||||
|
||||
// Hole alle verfügbaren Tile-Typen
|
||||
const tileTypes = await Match3TileType.findAll();
|
||||
console.log(`🎯 ${tileTypes.length} Tile-Typen verfügbar`);
|
||||
|
||||
// Erstelle eine Mapping-Tabelle für die alten tileTypes Arrays
|
||||
const tileTypeMapping = {
|
||||
'gem': tileTypes.find(t => t.name === 'gem'),
|
||||
'star': tileTypes.find(t => t.name === 'star'),
|
||||
'heart': tileTypes.find(t => t.name === 'heart'),
|
||||
'diamond': tileTypes.find(t => t.name === 'diamond'),
|
||||
'circle': tileTypes.find(t => t.name === 'circle'),
|
||||
'square': tileTypes.find(t => t.name === 'square'),
|
||||
'crown': tileTypes.find(t => t.name === 'crown'),
|
||||
'rainbow': tileTypes.find(t => t.name === 'rainbow')
|
||||
};
|
||||
|
||||
// Gehe durch alle Level und erstelle Verknüpfungen
|
||||
for (const level of levels) {
|
||||
console.log(`🔄 Verarbeite Level ${level.order}: ${level.name}`);
|
||||
|
||||
// Lösche bestehende Verknüpfungen für dieses Level
|
||||
await Match3LevelTileType.destroy({
|
||||
where: { levelId: level.id }
|
||||
});
|
||||
|
||||
// Verwende die alten tileTypes Arrays als Fallback
|
||||
let levelTileTypes = [];
|
||||
if (level.tileTypes && Array.isArray(level.tileTypes)) {
|
||||
levelTileTypes = level.tileTypes;
|
||||
} else {
|
||||
// Fallback: Verwende Standard-Tile-Typen basierend auf der Level-Nummer
|
||||
switch (level.order) {
|
||||
case 1:
|
||||
levelTileTypes = ['gem', 'star', 'heart'];
|
||||
break;
|
||||
case 2:
|
||||
levelTileTypes = ['gem', 'star', 'heart', 'diamond'];
|
||||
break;
|
||||
case 3:
|
||||
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle'];
|
||||
break;
|
||||
case 4:
|
||||
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle', 'square'];
|
||||
break;
|
||||
case 5:
|
||||
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle', 'square', 'crown'];
|
||||
break;
|
||||
case 6:
|
||||
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle', 'square', 'crown', 'rainbow'];
|
||||
break;
|
||||
default:
|
||||
levelTileTypes = ['gem', 'star', 'heart'];
|
||||
}
|
||||
}
|
||||
|
||||
// Erstelle Verknüpfungen für jeden Tile-Typ
|
||||
for (const tileTypeName of levelTileTypes) {
|
||||
const tileType = tileTypeMapping[tileTypeName];
|
||||
if (tileType) {
|
||||
await Match3LevelTileType.create({
|
||||
levelId: level.id,
|
||||
tileTypeId: tileType.id,
|
||||
weight: 1, // Standard-Gewichtung
|
||||
isActive: true
|
||||
});
|
||||
console.log(` ✅ Verknüpft mit ${tileType.displayName}`);
|
||||
} else {
|
||||
console.warn(` ⚠️ Tile-Typ "${tileTypeName}" nicht gefunden`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎉 Alle Match3 Level erfolgreich mit Tile-Typen verknüpft!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Aktualisieren der Match3 Level:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Führe die Aktualisierung aus, wenn das Skript direkt aufgerufen wird
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
updateMatch3LevelsWithTileTypes()
|
||||
.then(() => {
|
||||
console.log('✅ Level-Tile-Typ-Aktualisierung abgeschlossen');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler bei der Level-Tile-Typ-Aktualisierung:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export default updateMatch3LevelsWithTileTypes;
|
||||
1
frontend/public/assets/index-BLkCbvUa.css
Normal file
1
frontend/public/assets/index-BLkCbvUa.css
Normal file
File diff suppressed because one or more lines are too long
387
frontend/public/assets/index-BeXHiB-n.js
Normal file
387
frontend/public/assets/index-BeXHiB-n.js
Normal file
File diff suppressed because one or more lines are too long
379
frontend/public/assets/index-C2z2YL6l.js
Normal file
379
frontend/public/assets/index-C2z2YL6l.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/public/assets/index-z5H98VOo.css
Normal file
1
frontend/public/assets/index-z5H98VOo.css
Normal file
File diff suppressed because one or more lines are too long
BIN
frontend/public/assets/message24-DprhQEAI.png
Normal file
BIN
frontend/public/assets/message24-DprhQEAI.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
15
frontend/public/index.html
Normal file
15
frontend/public/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>YourPart</title>
|
||||
<script type="module" crossorigin src="/assets/index-C2z2YL6l.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BLkCbvUa.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -93,6 +93,61 @@
|
||||
"systemmessage": "Systemnachricht"
|
||||
},
|
||||
"confirmDelete": "Soll dieser Chatraum wirklich gelöscht werden?"
|
||||
},
|
||||
"match3": {
|
||||
"title": "Match3 Level verwalten",
|
||||
"newLevel": "Neues Level erstellen",
|
||||
"editLevel": "Level bearbeiten",
|
||||
"deleteLevel": "Level löschen",
|
||||
"confirmDelete": "Möchtest du dieses Level wirklich löschen?",
|
||||
"levelName": "Name",
|
||||
"levelDescription": "Beschreibung",
|
||||
"boardWidth": "Breite",
|
||||
"boardHeight": "Höhe",
|
||||
"moveLimit": "Zug-Limit",
|
||||
"levelOrder": "Reihenfolge",
|
||||
"boardLayout": "Board-Layout",
|
||||
"tileTypes": "Verfügbare Tile-Typen",
|
||||
"actions": "Aktionen",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"update": "Aktualisieren",
|
||||
"create": "Erstellen",
|
||||
"boardControls": {
|
||||
"fillAll": "Alle aktivieren",
|
||||
"clearAll": "Alle deaktivieren",
|
||||
"invert": "Invertieren"
|
||||
},
|
||||
"loading": "Lade Level...",
|
||||
"retry": "Erneut versuchen",
|
||||
"availableLevels": "Verfügbare Level: {count}",
|
||||
"levelFormat": "Level {number}: {name}",
|
||||
"levelObjectives": "Level-Objekte",
|
||||
"objectivesTitle": "Siegvoraussetzungen",
|
||||
"addObjective": "Objektiv hinzufügen",
|
||||
"removeObjective": "Entfernen",
|
||||
"objectiveType": "Typ",
|
||||
"objectiveTypeScore": "Punkte sammeln",
|
||||
"objectiveTypeMatches": "Matches machen",
|
||||
"objectiveTypeMoves": "Züge verwenden",
|
||||
"objectiveTypeTime": "Zeit einhalten",
|
||||
"objectiveTypeSpecial": "Spezialziel",
|
||||
"objectiveOperator": "Operator",
|
||||
"operatorGreaterEqual": "Größer oder gleich (≥)",
|
||||
"operatorLessEqual": "Kleiner oder gleich (≤)",
|
||||
"operatorEqual": "Gleich (=)",
|
||||
"operatorGreater": "Größer als (>)",
|
||||
"operatorLess": "Kleiner als (<)",
|
||||
"objectiveTarget": "Zielwert",
|
||||
"objectiveTargetPlaceholder": "z.B. 100",
|
||||
"objectiveOrder": "Reihenfolge",
|
||||
"objectiveOrderPlaceholder": "1, 2, 3...",
|
||||
"objectiveDescription": "Beschreibung",
|
||||
"objectiveDescriptionPlaceholder": "z.B. Sammle 100 Punkte",
|
||||
"objectiveRequired": "Erforderlich für Level-Abschluss",
|
||||
"noObjectives": "Keine Siegvoraussetzungen definiert. Klicke auf 'Objektiv hinzufügen' um welche zu erstellen."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,12 @@
|
||||
"logentries": "Log-Einträge",
|
||||
"edituser": "Benutzer bearbeiten",
|
||||
"database": "Datenbank"
|
||||
}
|
||||
},
|
||||
"minigames": "Minispiele",
|
||||
"m-minigames": {
|
||||
"match3": "Match3 Level"
|
||||
},
|
||||
"chatrooms": "Chaträume"
|
||||
},
|
||||
"m-friends": {
|
||||
"manageFriends": "Freunde verwalten",
|
||||
|
||||
@@ -1,3 +1,59 @@
|
||||
{
|
||||
|
||||
"admin": {
|
||||
"match3": {
|
||||
"title": "Manage Match3 Levels",
|
||||
"newLevel": "Create New Level",
|
||||
"editLevel": "Edit Level",
|
||||
"deleteLevel": "Delete Level",
|
||||
"confirmDelete": "Do you really want to delete this level?",
|
||||
"levelName": "Name",
|
||||
"levelDescription": "Description",
|
||||
"boardWidth": "Width",
|
||||
"boardHeight": "Height",
|
||||
"moveLimit": "Move Limit",
|
||||
"levelOrder": "Order",
|
||||
"boardLayout": "Board Layout",
|
||||
"tileTypes": "Available Tile Types",
|
||||
"actions": "Actions",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"update": "Update",
|
||||
"create": "Create",
|
||||
"boardControls": {
|
||||
"fillAll": "Activate All",
|
||||
"clearAll": "Deactivate All",
|
||||
"invert": "Invert"
|
||||
},
|
||||
"loading": "Loading levels...",
|
||||
"retry": "Retry",
|
||||
"availableLevels": "Available Levels: {count}",
|
||||
"levelFormat": "Level {number}: {name}",
|
||||
"levelObjectives": "Level Objectives",
|
||||
"objectivesTitle": "Victory Conditions",
|
||||
"addObjective": "Add Objective",
|
||||
"removeObjective": "Remove",
|
||||
"objectiveType": "Type",
|
||||
"objectiveTypeScore": "Collect Score",
|
||||
"objectiveTypeMatches": "Make Matches",
|
||||
"objectiveTypeMoves": "Use Moves",
|
||||
"objectiveTypeTime": "Keep Time",
|
||||
"objectiveTypeSpecial": "Special Goal",
|
||||
"objectiveOperator": "Operator",
|
||||
"operatorGreaterEqual": "Greater or equal (≥)",
|
||||
"operatorLessEqual": "Less or equal (≤)",
|
||||
"operatorEqual": "Equal (=)",
|
||||
"operatorGreater": "Greater than (>)",
|
||||
"operatorLess": "Less than (<)",
|
||||
"objectiveTarget": "Target Value",
|
||||
"objectiveTargetPlaceholder": "e.g. 100",
|
||||
"objectiveOrder": "Order",
|
||||
"objectiveOrderPlaceholder": "1, 2, 3...",
|
||||
"objectiveDescription": "Description",
|
||||
"objectiveDescriptionPlaceholder": "e.g. Collect 100 points",
|
||||
"objectiveRequired": "Required for level completion",
|
||||
"noObjectives": "No victory conditions defined. Click 'Add Objective' to create some."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import AdminInterestsView from '../views/admin/InterestsView.vue';
|
||||
import AdminContactsView from '../views/admin/ContactsView.vue';
|
||||
import RoomsView from '../views/admin/RoomsView.vue';
|
||||
import ForumAdminView from '../dialogues/admin/ForumAdminView.vue';
|
||||
import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue'
|
||||
import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue';
|
||||
import AdminMinigamesView from '../views/admin/MinigamesView.vue';
|
||||
|
||||
const adminRoutes = [
|
||||
{
|
||||
@@ -34,6 +35,12 @@ const adminRoutes = [
|
||||
name: 'AdminFalukantEditUserView',
|
||||
component: AdminFalukantEditUserView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/minigames/match3',
|
||||
name: 'AdminMinigames',
|
||||
component: AdminMinigamesView,
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -10,10 +10,19 @@ const apiClient = axios.create({
|
||||
|
||||
apiClient.interceptors.request.use(config => {
|
||||
const user = store.getters.user;
|
||||
console.log('🔑 Axios Interceptor - User:', user);
|
||||
|
||||
if (user && user.authCode) {
|
||||
config.headers['userId'] = user.id;
|
||||
config.headers['authCode'] = user.authCode;
|
||||
config.headers['userid'] = user.id;
|
||||
config.headers['authcode'] = user.authCode; // Kleinschreibung!
|
||||
console.log('📡 Setze Headers:', {
|
||||
userid: user.id,
|
||||
authcode: user.authCode
|
||||
});
|
||||
} else {
|
||||
console.log('⚠️ Keine User-Daten verfügbar');
|
||||
}
|
||||
|
||||
return config;
|
||||
}, error => {
|
||||
return Promise.reject(error);
|
||||
|
||||
1517
frontend/src/views/admin/MinigamesView.vue
Normal file
1517
frontend/src/views/admin/MinigamesView.vue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user