1512 lines
74 KiB
Java
1512 lines
74 KiB
Java
package de.ticketsystem.database;
|
||
|
||
import java.io.File;
|
||
import java.io.IOException;
|
||
import org.bukkit.configuration.file.YamlConfiguration;
|
||
import org.json.simple.JSONArray;
|
||
import org.json.simple.JSONObject;
|
||
import org.json.simple.parser.JSONParser;
|
||
import com.zaxxer.hikari.HikariConfig;
|
||
import com.zaxxer.hikari.HikariDataSource;
|
||
import de.ticketsystem.TicketPlugin;
|
||
import de.ticketsystem.model.Ticket;
|
||
|
||
import de.ticketsystem.model.TicketComment;
|
||
import de.ticketsystem.model.TicketPriority;
|
||
import de.ticketsystem.model.TicketStatus;
|
||
import java.sql.*;
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
import java.util.UUID;
|
||
import java.util.logging.Level;
|
||
import java.io.FileReader;
|
||
import java.io.FileWriter;
|
||
import org.bukkit.Bukkit;
|
||
|
||
public class DatabaseManager {
|
||
|
||
// ─────────────────────────── Felder ────────────────────────────────────
|
||
|
||
private final TicketPlugin plugin;
|
||
private HikariDataSource dataSource;
|
||
private boolean useMySQL;
|
||
private boolean useJson;
|
||
private File dataFile;
|
||
private YamlConfiguration dataConfig;
|
||
private JSONArray dataJson;
|
||
private String dataFileName;
|
||
private String archiveFileName;
|
||
|
||
// ─────────────────────────── Konstruktoren ─────────────────────────────
|
||
|
||
public DatabaseManager(TicketPlugin plugin) {
|
||
this.plugin = plugin;
|
||
this.useMySQL = plugin.getConfig().getBoolean("use-mysql", true);
|
||
this.useJson = plugin.getConfig().getBoolean("use-json", false);
|
||
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] DatabaseManager initialisiert. useMySQL=" + useMySQL + ", useJson=" + useJson);
|
||
|
||
String dataPath = plugin.getConfig().getString("data-file", useJson ? "data.json" : "data.yml");
|
||
String archivePath = plugin.getConfig().getString("archive-file", "archive.json");
|
||
this.dataFileName = dataPath;
|
||
this.archiveFileName = archivePath;
|
||
|
||
if (!useMySQL) {
|
||
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] Datei-Speicher wird verwendet: " + dataPath);
|
||
if (useJson) {
|
||
dataFile = resolvePath(dataPath);
|
||
if (!dataFile.exists()) {
|
||
try {
|
||
dataFile.getParentFile().mkdirs();
|
||
dataFile.createNewFile();
|
||
dataJson = new JSONArray();
|
||
} catch (IOException e) { sendError("Konnte " + dataPath + " nicht erstellen: " + e.getMessage()); }
|
||
} else {
|
||
try {
|
||
dataJson = (JSONArray) new JSONParser().parse(new FileReader(dataFile));
|
||
} catch (Exception e) { sendError("Konnte " + dataPath + " nicht laden: " + e.getMessage()); dataJson = new JSONArray(); }
|
||
}
|
||
} else {
|
||
dataFile = resolvePath(dataPath);
|
||
if (!dataFile.exists()) {
|
||
try { dataFile.getParentFile().mkdirs(); dataFile.createNewFile(); }
|
||
catch (IOException e) { sendError("Konnte " + dataPath + " nicht erstellen: " + e.getMessage()); }
|
||
}
|
||
dataConfig = YamlConfiguration.loadConfiguration(dataFile);
|
||
}
|
||
validateLoadedTickets();
|
||
}
|
||
}
|
||
|
||
public DatabaseManager(File dataFile, YamlConfiguration dataConfig) {
|
||
this.plugin = null;
|
||
this.useMySQL = false;
|
||
this.useJson = false;
|
||
this.dataFileName = dataFile.getName();
|
||
this.archiveFileName = "archive.json";
|
||
this.dataFile = dataFile;
|
||
this.dataConfig = dataConfig;
|
||
validateLoadedTickets();
|
||
}
|
||
|
||
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
|
||
|
||
private File resolvePath(String path) {
|
||
File f = new File(path);
|
||
if (f.isAbsolute()) return f;
|
||
return new File(plugin != null ? plugin.getDataFolder() : new File("."), path);
|
||
}
|
||
|
||
private void sendError(String msg) {
|
||
if (plugin != null) plugin.getLogger().severe(msg);
|
||
if (Bukkit.getServer() != null) {
|
||
Bukkit.getOnlinePlayers().stream()
|
||
.filter(p -> p.hasPermission("ticket.admin"))
|
||
.forEach(p -> p.sendMessage("§c[TicketSystem] " + msg));
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────── Verbindung ────────────────────────────────
|
||
|
||
public boolean connect() {
|
||
if (useMySQL) {
|
||
try {
|
||
HikariConfig config = new HikariConfig();
|
||
config.setJdbcUrl(String.format(
|
||
"jdbc:mysql://%s:%d/%s?useSSL=false&autoReconnect=true&characterEncoding=UTF-8",
|
||
plugin.getConfig().getString("mysql.host"),
|
||
plugin.getConfig().getInt("mysql.port"),
|
||
plugin.getConfig().getString("mysql.database")));
|
||
config.setUsername(plugin.getConfig().getString("mysql.username"));
|
||
config.setPassword(plugin.getConfig().getString("mysql.password"));
|
||
config.setMaximumPoolSize(plugin.getConfig().getInt("mysql.pool-size", 10));
|
||
config.setConnectionTimeout(plugin.getConfig().getLong("mysql.connection-timeout", 30000));
|
||
config.setPoolName("TicketSystem-Pool");
|
||
config.addDataSourceProperty("cachePrepStmts", "true");
|
||
config.addDataSourceProperty("prepStmtCacheSize", "250");
|
||
config.addDataSourceProperty("prepStmtCacheSqlLimit","2048");
|
||
|
||
dataSource = new HikariDataSource(config);
|
||
createTables();
|
||
ensureColumns();
|
||
plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt.");
|
||
return true;
|
||
} catch (Exception e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e);
|
||
plugin.getLogger().warning("Weiche auf Datei-Speicherung (data.yml) aus!");
|
||
useMySQL = false;
|
||
dataFile = new File(plugin.getDataFolder(), "data.yml");
|
||
if (!dataFile.exists()) {
|
||
try { dataFile.getParentFile().mkdirs(); dataFile.createNewFile(); }
|
||
catch (IOException ex) { plugin.getLogger().severe("Konnte data.yml nicht erstellen: " + ex.getMessage()); }
|
||
}
|
||
dataConfig = YamlConfiguration.loadConfiguration(dataFile);
|
||
return true;
|
||
}
|
||
} else {
|
||
plugin.getLogger().info("MySQL deaktiviert. Verwende Datei-Speicherung (data.yml).");
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public void disconnect() {
|
||
if (useMySQL && dataSource != null && !dataSource.isClosed()) {
|
||
dataSource.close();
|
||
plugin.getLogger().info("MySQL-Verbindung getrennt.");
|
||
}
|
||
}
|
||
|
||
private Connection getConnection() throws SQLException { return dataSource.getConnection(); }
|
||
|
||
// ─────────────────────────── Tabellen erstellen ────────────────────────
|
||
|
||
private void createTables() {
|
||
// Haupt-Tickets-Tabelle
|
||
// BungeeCord: server_name speichert auf welchem Server das Ticket erstellt wurde
|
||
String ticketsSql = """
|
||
CREATE TABLE IF NOT EXISTS tickets (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
creator_uuid VARCHAR(36) NOT NULL,
|
||
creator_name VARCHAR(16) NOT NULL,
|
||
message VARCHAR(255) NOT NULL,
|
||
world VARCHAR(64) NOT NULL,
|
||
x DOUBLE NOT NULL,
|
||
y DOUBLE NOT NULL,
|
||
z DOUBLE NOT NULL,
|
||
yaw FLOAT NOT NULL DEFAULT 0,
|
||
pitch FLOAT NOT NULL DEFAULT 0,
|
||
status VARCHAR(16) NOT NULL DEFAULT 'OPEN',
|
||
claimer_uuid VARCHAR(36),
|
||
claimer_name VARCHAR(16),
|
||
forwarded_to_uuid VARCHAR(36),
|
||
forwarded_to_name VARCHAR(16),
|
||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
claimed_at TIMESTAMP NULL,
|
||
closed_at TIMESTAMP NULL,
|
||
close_comment VARCHAR(500) NULL,
|
||
player_deleted BOOLEAN DEFAULT FALSE,
|
||
category VARCHAR(16) NOT NULL DEFAULT 'GENERAL',
|
||
priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL',
|
||
player_rating VARCHAR(16) NULL,
|
||
claimer_notified BOOLEAN DEFAULT FALSE,
|
||
close_notified BOOLEAN DEFAULT FALSE,
|
||
server_name VARCHAR(64) NOT NULL DEFAULT 'unknown'
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
""";
|
||
|
||
// Kommentare-Tabelle
|
||
String commentsSql = """
|
||
CREATE TABLE IF NOT EXISTS ticket_comments (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
ticket_id INT NOT NULL,
|
||
author_uuid VARCHAR(36) NOT NULL,
|
||
author_name VARCHAR(16) NOT NULL,
|
||
message VARCHAR(500) NOT NULL,
|
||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_ticket_id (ticket_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
""";
|
||
|
||
// Blacklist-Tabelle
|
||
String blacklistSql = """
|
||
CREATE TABLE IF NOT EXISTS ticket_blacklist (
|
||
uuid VARCHAR(36) NOT NULL PRIMARY KEY,
|
||
player_name VARCHAR(16) NOT NULL,
|
||
reason VARCHAR(255) DEFAULT '',
|
||
banned_by VARCHAR(16) NOT NULL,
|
||
banned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
""";
|
||
|
||
// Ausstehende Benachrichtigungen für Offline-Spieler
|
||
String notifSql = """
|
||
CREATE TABLE IF NOT EXISTS ticket_pending_notifications (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
player_uuid VARCHAR(36) NOT NULL,
|
||
message VARCHAR(512) NOT NULL,
|
||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_player_uuid (player_uuid)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
""";
|
||
|
||
// Persistente Statistik-Tabelle – überlebt das Löschen/Archivieren von Tickets.
|
||
// Wird beim Schließen eines Tickets befüllt und beim Bewerten aktualisiert.
|
||
// So gehen keine Zahlen verloren wenn das Archiv geleert wird.
|
||
String statsSql = """
|
||
CREATE TABLE IF NOT EXISTS ticket_stats (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
ticket_id INT NOT NULL,
|
||
claimer_uuid VARCHAR(36) NULL,
|
||
claimer_name VARCHAR(16) NULL,
|
||
creator_uuid VARCHAR(36) NOT NULL,
|
||
creator_name VARCHAR(16) NOT NULL,
|
||
category VARCHAR(16) NOT NULL DEFAULT 'general',
|
||
priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL',
|
||
server_name VARCHAR(64) NOT NULL DEFAULT 'unknown',
|
||
player_rating VARCHAR(16) NULL,
|
||
closed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_claimer_uuid (claimer_uuid),
|
||
INDEX idx_closed_at (closed_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
""";
|
||
|
||
// Ausstehende BungeeCord-Teleport-Aufträge.
|
||
// Wird gesetzt wenn ein Admin via GUI/Command auf einen anderen Server teleportiert.
|
||
// PlayerJoinListener liest den Eintrag beim Ankommen, teleportiert, löscht ihn dann.
|
||
String pendingTeleportSql = """
|
||
CREATE TABLE IF NOT EXISTS ticket_pending_teleport (
|
||
player_uuid VARCHAR(36) NOT NULL PRIMARY KEY,
|
||
world VARCHAR(64) NOT NULL,
|
||
x DOUBLE NOT NULL,
|
||
y DOUBLE NOT NULL,
|
||
z DOUBLE NOT NULL,
|
||
yaw FLOAT NOT NULL DEFAULT 0,
|
||
pitch FLOAT NOT NULL DEFAULT 0,
|
||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
""";
|
||
|
||
// Persistente Ersteller-Statistik – zählt alle jemals erstellten Tickets pro Spieler.
|
||
// Überlebt das Löschen und Archivieren von Tickets vollständig.
|
||
// Wird bei jedem createTicket() inkrementiert (INSERT … ON DUPLICATE KEY UPDATE).
|
||
String creatorStatsSql = """
|
||
CREATE TABLE IF NOT EXISTS ticket_creator_stats (
|
||
creator_uuid VARCHAR(36) NOT NULL PRIMARY KEY,
|
||
creator_name VARCHAR(16) NOT NULL,
|
||
ticket_count INT NOT NULL DEFAULT 1,
|
||
last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
ON UPDATE CURRENT_TIMESTAMP,
|
||
INDEX idx_ticket_count (ticket_count)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
""";
|
||
|
||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||
stmt.execute(ticketsSql);
|
||
stmt.execute(commentsSql);
|
||
stmt.execute(blacklistSql);
|
||
stmt.execute(notifSql);
|
||
stmt.execute(statsSql);
|
||
stmt.execute(pendingTeleportSql);
|
||
stmt.execute(creatorStatsSql);
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ergänzt fehlende Spalten in bestehenden Datenbanken automatisch.
|
||
* Wichtig für Upgrades von älteren Versionen.
|
||
*/
|
||
private void ensureColumns() {
|
||
ensureColumn("close_comment", "ALTER TABLE tickets ADD COLUMN close_comment VARCHAR(500) NULL");
|
||
ensureColumn("player_deleted", "ALTER TABLE tickets ADD COLUMN player_deleted BOOLEAN DEFAULT FALSE");
|
||
ensureColumn("category", "ALTER TABLE tickets ADD COLUMN category VARCHAR(16) NOT NULL DEFAULT 'GENERAL'");
|
||
ensureColumn("priority", "ALTER TABLE tickets ADD COLUMN priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL'");
|
||
ensureColumn("player_rating", "ALTER TABLE tickets ADD COLUMN player_rating VARCHAR(16) NULL");
|
||
ensureColumn("claimer_notified", "ALTER TABLE tickets ADD COLUMN claimer_notified BOOLEAN DEFAULT FALSE");
|
||
// Bug-Fix: close_notified verhindert Duplikat-Discord-Nachrichten und doppelte Offline-Benachrichtigungen bei Server-Wechsel
|
||
ensureColumn("close_notified", "ALTER TABLE tickets ADD COLUMN close_notified BOOLEAN DEFAULT FALSE");
|
||
// BungeeCord: Server-Name-Spalte für bestehende Datenbanken nachrüsten
|
||
ensureColumn("server_name", "ALTER TABLE tickets ADD COLUMN server_name VARCHAR(64) NOT NULL DEFAULT 'unknown'");
|
||
|
||
// ticket_stats: Spalte player_rating nachrüsten falls Tabelle vor diesem Feature existiert
|
||
ensureStatsColumn("player_rating", "ALTER TABLE ticket_stats ADD COLUMN player_rating VARCHAR(16) NULL");
|
||
}
|
||
|
||
private void ensureColumn(String columnName, String alterSql) {
|
||
String checkSql = """
|
||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||
WHERE TABLE_SCHEMA = DATABASE()
|
||
AND TABLE_NAME = 'tickets'
|
||
AND COLUMN_NAME = ?
|
||
""";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(checkSql)) {
|
||
ps.setString(1, columnName);
|
||
ResultSet rs = ps.executeQuery();
|
||
if (rs.next() && rs.getInt(1) == 0) {
|
||
try (Statement stmt = conn.createStatement()) {
|
||
stmt.execute(alterSql);
|
||
plugin.getLogger().info("[TicketSystem] Spalte '" + columnName + "' wurde zur Datenbank hinzugefügt.");
|
||
}
|
||
}
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureColumn(" + columnName + "): " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
private void ensureStatsColumn(String columnName, String alterSql) {
|
||
String checkSql = """
|
||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||
WHERE TABLE_SCHEMA = DATABASE()
|
||
AND TABLE_NAME = 'ticket_stats'
|
||
AND COLUMN_NAME = ?
|
||
""";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(checkSql)) {
|
||
ps.setString(1, columnName);
|
||
ResultSet rs = ps.executeQuery();
|
||
if (rs.next() && rs.getInt(1) == 0) {
|
||
try (Statement stmt = conn.createStatement()) {
|
||
stmt.execute(alterSql);
|
||
plugin.getLogger().info("[TicketSystem] Stats-Spalte '" + columnName + "' wurde hinzugefügt.");
|
||
}
|
||
}
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureStatsColumn(" + columnName + "): " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────── CRUD Tickets ──────────────────────────────
|
||
|
||
public int createTicket(Ticket ticket) {
|
||
if (useMySQL) {
|
||
// BungeeCord: server_name wird ebenfalls gespeichert
|
||
String sql = """
|
||
INSERT INTO tickets (creator_uuid, creator_name, message, world, x, y, z, yaw, pitch,
|
||
category, priority, server_name)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""";
|
||
try (Connection conn = getConnection();
|
||
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||
ps.setString(1, ticket.getCreatorUUID().toString());
|
||
ps.setString(2, ticket.getCreatorName());
|
||
ps.setString(3, ticket.getMessage());
|
||
ps.setString(4, ticket.getWorldName());
|
||
ps.setDouble(5, ticket.getX());
|
||
ps.setDouble(6, ticket.getY());
|
||
ps.setDouble(7, ticket.getZ());
|
||
ps.setFloat(8, ticket.getYaw());
|
||
ps.setFloat(9, ticket.getPitch());
|
||
ps.setString(10, ticket.getCategoryKey());
|
||
ps.setString(11, ticket.getPriority().name());
|
||
ps.setString(12, ticket.getServerName()); // BungeeCord
|
||
ps.executeUpdate();
|
||
ResultSet rs = ps.getGeneratedKeys();
|
||
if (rs.next()) {
|
||
int newId = rs.getInt(1);
|
||
incrementCreatorStats(ticket.getCreatorUUID(), ticket.getCreatorName());
|
||
return newId;
|
||
}
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen des Tickets: " + e.getMessage(), e);
|
||
}
|
||
return -1;
|
||
} else {
|
||
int id = dataConfig.getInt("lastId", 0) + 1;
|
||
ticket.setId(id);
|
||
dataConfig.set("lastId", id);
|
||
dataConfig.set("tickets." + id, ticket);
|
||
// Datei-Modus: Zähler in dataConfig pflegen
|
||
String statsKey = "creator-stats." + ticket.getCreatorUUID().toString();
|
||
int current = dataConfig.getInt(statsKey + ".count", 0);
|
||
dataConfig.set(statsKey + ".count", current + 1);
|
||
dataConfig.set(statsKey + ".name", ticket.getCreatorName());
|
||
saveDataConfig();
|
||
return id;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────── Creator-Leaderboard ────────────────────────
|
||
|
||
/**
|
||
* Erhöht den Ticket-Zähler eines Erstellers um 1 (MySQL-Modus).
|
||
* Wird nach jedem erfolgreichen createTicket() aufgerufen.
|
||
* Die Tabelle ist unabhängig von tickets/archive – Zahlen gehen nie verloren.
|
||
*/
|
||
private void incrementCreatorStats(UUID creatorUUID, String creatorName) {
|
||
if (!useMySQL) return;
|
||
String sql = """
|
||
INSERT INTO ticket_creator_stats (creator_uuid, creator_name, ticket_count)
|
||
VALUES (?, ?, 1)
|
||
ON DUPLICATE KEY UPDATE
|
||
ticket_count = ticket_count + 1,
|
||
creator_name = VALUES(creator_name)
|
||
""";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, creatorUUID.toString());
|
||
ps.setString(2, creatorName);
|
||
ps.executeUpdate();
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Aktualisieren der Creator-Stats: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gibt die Top-{limit} Ticket-Ersteller zurück.
|
||
* Jeder Eintrag: [rank, playerName, ticketCount] (alle als String)
|
||
* Funktioniert in MySQL- und Datei-Modus.
|
||
*/
|
||
public List<String[]> getTopCreators(int limit) {
|
||
List<String[]> result = new ArrayList<>();
|
||
if (useMySQL) {
|
||
String sql = """
|
||
SELECT creator_name, ticket_count
|
||
FROM ticket_creator_stats
|
||
ORDER BY ticket_count DESC
|
||
LIMIT ?
|
||
""";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setInt(1, limit);
|
||
ResultSet rs = ps.executeQuery();
|
||
int rank = 1;
|
||
while (rs.next()) {
|
||
result.add(new String[]{
|
||
String.valueOf(rank++),
|
||
rs.getString("creator_name"),
|
||
String.valueOf(rs.getInt("ticket_count"))
|
||
});
|
||
}
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Abrufen der Top-Creator: " + e.getMessage(), e);
|
||
}
|
||
} else if (dataConfig != null && dataConfig.contains("creator-stats")) {
|
||
// Datei-Modus: alle Einträge einlesen, nach count absteigend sortieren, begrenzen
|
||
record Entry(String name, int count) {}
|
||
List<Entry> entries = new ArrayList<>();
|
||
for (String uuid : dataConfig.getConfigurationSection("creator-stats").getKeys(false)) {
|
||
String name = dataConfig.getString("creator-stats." + uuid + ".name", uuid);
|
||
int count = dataConfig.getInt("creator-stats." + uuid + ".count", 0);
|
||
entries.add(new Entry(name, count));
|
||
}
|
||
entries.sort((a, b) -> Integer.compare(b.count(), a.count()));
|
||
int rank = 1;
|
||
for (Entry e : entries.subList(0, Math.min(limit, entries.size()))) {
|
||
result.add(new String[]{String.valueOf(rank++), e.name(), String.valueOf(e.count())});
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ── BUG FIX #1 ──────────────────────────────────────────────────────────
|
||
// Vorher: WHERE id = ? AND status = 'OPEN'
|
||
// Problem: Ein FORWARDED-Ticket konnte nicht geclaimed werden – das UPDATE
|
||
// schlug lautlos fehl, claimer_uuid/claimer_name wurden nie geschrieben.
|
||
// Fix: WHERE id = ? AND status != 'CLOSED'
|
||
// Damit können sowohl OPEN als auch FORWARDED Tickets korrekt
|
||
// angenommen werden und claimer_uuid/claimer_name werden immer gesetzt.
|
||
// ────────────────────────────────────────────────────────────────────────
|
||
public boolean claimTicket(int ticketId, UUID claimerUUID, String claimerName) {
|
||
if (useMySQL) {
|
||
String sql = """
|
||
UPDATE tickets
|
||
SET status = 'CLAIMED', claimer_uuid = ?, claimer_name = ?,
|
||
claimed_at = NOW(), player_deleted = FALSE
|
||
WHERE id = ? AND status != 'CLOSED'
|
||
""";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, claimerUUID.toString());
|
||
ps.setString(2, claimerName);
|
||
ps.setInt(3, ticketId);
|
||
return ps.executeUpdate() > 0;
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Claimen des Tickets: " + e.getMessage(), e);
|
||
}
|
||
return false;
|
||
} else {
|
||
Ticket t = getTicketById(ticketId);
|
||
if (t == null || t.getStatus() == TicketStatus.CLOSED) return false;
|
||
t.setStatus(TicketStatus.CLAIMED);
|
||
t.setClaimerUUID(claimerUUID);
|
||
t.setClaimerName(claimerName);
|
||
t.setClaimedAt(new Timestamp(System.currentTimeMillis()));
|
||
t.setPlayerDeleted(false);
|
||
dataConfig.set("tickets." + ticketId, t);
|
||
saveDataConfig();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public boolean closeTicket(int ticketId, String closeComment) {
|
||
if (useMySQL) {
|
||
String sql = """
|
||
UPDATE tickets SET status = 'CLOSED', closed_at = NOW(), close_comment = ?
|
||
WHERE id = ? AND status != 'CLOSED'
|
||
""";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, closeComment != null ? closeComment : "");
|
||
ps.setInt(2, ticketId);
|
||
return ps.executeUpdate() > 0;
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Schließen des Tickets: " + e.getMessage(), e);
|
||
}
|
||
return false;
|
||
} else {
|
||
Ticket t = getTicketById(ticketId);
|
||
if (t == null || t.getStatus() == TicketStatus.CLOSED) return false;
|
||
t.setStatus(TicketStatus.CLOSED);
|
||
t.setClosedAt(new Timestamp(System.currentTimeMillis()));
|
||
t.setCloseComment(closeComment != null ? closeComment : "");
|
||
dataConfig.set("tickets." + ticketId, t);
|
||
saveDataConfig();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public boolean markAsPlayerDeleted(int id) {
|
||
if (useMySQL) {
|
||
String sql = "UPDATE tickets SET player_deleted = TRUE WHERE id = ?";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setInt(1, id);
|
||
return ps.executeUpdate() > 0;
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Markieren als gelöscht: " + e.getMessage(), e);
|
||
}
|
||
return false;
|
||
} else {
|
||
Ticket t = getTicketById(id);
|
||
if (t == null) return false;
|
||
t.setPlayerDeleted(true);
|
||
dataConfig.set("tickets." + id, t);
|
||
saveDataConfig();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public boolean setTicketPriority(int ticketId, TicketPriority priority) {
|
||
if (useMySQL) {
|
||
String sql = "UPDATE tickets SET priority = ? WHERE id = ?";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, priority.name());
|
||
ps.setInt(2, ticketId);
|
||
return ps.executeUpdate() > 0;
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Setzen der Priorität: " + e.getMessage(), e);
|
||
}
|
||
return false;
|
||
} else {
|
||
Ticket t = getTicketById(ticketId);
|
||
if (t == null) return false;
|
||
t.setPriority(priority);
|
||
dataConfig.set("tickets." + ticketId, t);
|
||
saveDataConfig();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public boolean deleteTicket(int id) {
|
||
if (useMySQL) {
|
||
String sql = "DELETE FROM tickets WHERE id = ?";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setInt(1, id);
|
||
return ps.executeUpdate() > 0;
|
||
} catch (SQLException e) {
|
||
sendError("Fehler beim Löschen des Tickets: " + e.getMessage());
|
||
}
|
||
return false;
|
||
} else {
|
||
if (!dataConfig.contains("tickets." + id)) return false;
|
||
dataConfig.set("tickets." + id, null);
|
||
saveDataConfig();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public boolean forwardTicket(int ticketId, UUID toUUID, String toName) {
|
||
if (useMySQL) {
|
||
String sql = """
|
||
UPDATE tickets
|
||
SET status = 'FORWARDED', forwarded_to_uuid = ?, forwarded_to_name = ?,
|
||
player_deleted = FALSE
|
||
WHERE id = ? AND status != 'CLOSED'
|
||
""";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, toUUID.toString());
|
||
ps.setString(2, toName);
|
||
ps.setInt(3, ticketId);
|
||
return ps.executeUpdate() > 0;
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Weiterleiten des Tickets: " + e.getMessage(), e);
|
||
}
|
||
return false;
|
||
} else {
|
||
Ticket t = getTicketById(ticketId);
|
||
if (t == null || t.getStatus() == TicketStatus.CLOSED) return false;
|
||
t.setStatus(TicketStatus.FORWARDED);
|
||
t.setForwardedToUUID(toUUID);
|
||
t.setForwardedToName(toName);
|
||
t.setPlayerDeleted(false);
|
||
dataConfig.set("tickets." + ticketId, t);
|
||
saveDataConfig();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────── Claim-Benachrichtigung markieren ──────────
|
||
|
||
public void markClaimerNotified(int ticketId) {
|
||
if (useMySQL) {
|
||
String sql = "UPDATE tickets SET claimer_notified = TRUE WHERE id = ?";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setInt(1, ticketId);
|
||
ps.executeUpdate();
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei markClaimerNotified: " + e.getMessage(), e);
|
||
}
|
||
} else {
|
||
Ticket t = getTicketById(ticketId);
|
||
if (t != null) {
|
||
t.setClaimerNotified(true);
|
||
dataConfig.set("tickets." + ticketId, t);
|
||
saveDataConfig();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────── Schließ-Benachrichtigung markieren ────────
|
||
|
||
/**
|
||
* Setzt close_notified = TRUE für ein Ticket (persistiert in DB/Datei).
|
||
* Verhindert Duplikat-Benachrichtigungen und doppelte Discord-Nachrichten
|
||
* bei Server-Wechseln in BungeeCord-Netzwerken.
|
||
*/
|
||
public void markCloseNotified(int ticketId) {
|
||
if (useMySQL) {
|
||
String sql = "UPDATE tickets SET close_notified = TRUE WHERE id = ?";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setInt(1, ticketId);
|
||
ps.executeUpdate();
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei markCloseNotified: " + e.getMessage(), e);
|
||
}
|
||
} else {
|
||
Ticket t = getTicketById(ticketId);
|
||
if (t != null) {
|
||
t.setCloseNotified(true);
|
||
dataConfig.set("tickets." + ticketId, t);
|
||
saveDataConfig();
|
||
}
|
||
}
|
||
}
|
||
|
||
public boolean rateTicket(int ticketId, String rating) {
|
||
if (useMySQL) {
|
||
String sql = "UPDATE tickets SET player_rating = ? WHERE id = ? AND status = 'CLOSED' AND player_rating IS NULL";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, rating);
|
||
ps.setInt(2, ticketId);
|
||
boolean updated = ps.executeUpdate() > 0;
|
||
// Bewertung auch in die persistente Stats-Tabelle übertragen
|
||
if (updated) updateStatsRating(ticketId, rating);
|
||
return updated;
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei rateTicket: " + e.getMessage(), e);
|
||
}
|
||
return false;
|
||
} else {
|
||
Ticket t = getTicketById(ticketId);
|
||
if (t == null || t.getStatus() != TicketStatus.CLOSED || t.hasRating()) return false;
|
||
t.setPlayerRating(rating);
|
||
dataConfig.set("tickets." + ticketId, t);
|
||
saveDataConfig();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Schreibt einen Eintrag in ticket_stats wenn ein Ticket geschlossen wird.
|
||
* Diese Tabelle bleibt dauerhaft erhalten – auch wenn das Ticket später
|
||
* gelöscht oder archiviert wird. So gehen Statistiken nie verloren.
|
||
*
|
||
* @param ticket Das gerade geschlossene Ticket-Objekt
|
||
* @param closerName Name des Admins/Supporters der das Ticket geschlossen hat
|
||
* (kann vom claimer_name abweichen wenn ein Admin fremde Tickets schließt)
|
||
*/
|
||
public void recordClosedTicket(Ticket ticket, String closerName) {
|
||
if (!useMySQL) return;
|
||
|
||
// closer_name bevorzugen – falls null, auf claimer_name zurückfallen
|
||
String effectiveCloser = (closerName != null && !closerName.isEmpty())
|
||
? closerName : ticket.getClaimerName();
|
||
String effectiveCloserUuid = null;
|
||
// UUID nur setzen wenn closer == claimer (sonst haben wir die UUID des Admins nicht direkt)
|
||
if (effectiveCloser != null && effectiveCloser.equals(ticket.getClaimerName())
|
||
&& ticket.getClaimerUUID() != null) {
|
||
effectiveCloserUuid = ticket.getClaimerUUID().toString();
|
||
}
|
||
|
||
String sql = """
|
||
INSERT INTO ticket_stats
|
||
(ticket_id, claimer_uuid, claimer_name, creator_uuid, creator_name,
|
||
category, priority, server_name, player_rating, closed_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||
ON DUPLICATE KEY UPDATE closed_at = closed_at
|
||
""";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setInt(1, ticket.getId());
|
||
ps.setString(2, effectiveCloserUuid);
|
||
ps.setString(3, effectiveCloser);
|
||
ps.setString(4, ticket.getCreatorUUID().toString());
|
||
ps.setString(5, ticket.getCreatorName());
|
||
ps.setString(6, ticket.getCategoryKey());
|
||
ps.setString(7, ticket.getPriority().name());
|
||
ps.setString(8, ticket.getServerName());
|
||
ps.setString(9, ticket.getPlayerRating());
|
||
ps.executeUpdate();
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei recordClosedTicket: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Aktualisiert die Bewertung in ticket_stats wenn ein Spieler sein Ticket bewertet.
|
||
* Wird von rateTicket() intern aufgerufen.
|
||
*/
|
||
private void updateStatsRating(int ticketId, String rating) {
|
||
String sql = "UPDATE ticket_stats SET player_rating = ? WHERE ticket_id = ?";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, rating);
|
||
ps.setInt(2, ticketId);
|
||
ps.executeUpdate();
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei updateStatsRating: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gibt eine Liste aller Support-Mitarbeiter mit ihren Bewertungsstatistiken zurück.
|
||
* Liest aus ticket_stats – unabhängig davon ob die Tickets noch in der DB existieren.
|
||
*
|
||
* Rückgabe: Liste von String-Arrays mit
|
||
* [0] claimer_name, [1] thumbsUp, [2] thumbsDown, [3] total, [4] prozent
|
||
*/
|
||
public List<String[]> getStaffRatings() {
|
||
List<String[]> result = new ArrayList<>();
|
||
if (!useMySQL) return result;
|
||
|
||
String sql = """
|
||
SELECT
|
||
claimer_name,
|
||
SUM(player_rating = 'THUMBS_UP') AS thumbs_up,
|
||
SUM(player_rating = 'THUMBS_DOWN') AS thumbs_down,
|
||
COUNT(*) AS total_closed
|
||
FROM ticket_stats
|
||
WHERE claimer_name IS NOT NULL
|
||
GROUP BY claimer_uuid, claimer_name
|
||
ORDER BY total_closed DESC
|
||
""";
|
||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||
ResultSet rs = stmt.executeQuery(sql);
|
||
while (rs.next()) {
|
||
int up = rs.getInt("thumbs_up");
|
||
int down = rs.getInt("thumbs_down");
|
||
int total = rs.getInt("total_closed");
|
||
int rated = up + down;
|
||
String percent = rated > 0 ? Math.round(up * 100.0 / rated) + "%" : "–";
|
||
result.add(new String[]{
|
||
rs.getString("claimer_name"),
|
||
String.valueOf(up),
|
||
String.valueOf(down),
|
||
String.valueOf(total),
|
||
percent
|
||
});
|
||
}
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei getStaffRatings: " + e.getMessage(), e);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ─────────────────────────── BungeeCord Pending-Teleport ───────────────
|
||
|
||
/**
|
||
* Speichert einen ausstehenden Teleport-Auftrag für einen Admin/Supporter.
|
||
* Wird aufgerufen bevor der Spieler via BungeeCord auf den Ziel-Server
|
||
* geschickt wird. Der PlayerJoinListener liest den Eintrag beim Ankommen.
|
||
*/
|
||
public void setPendingTeleport(UUID playerUUID, String world,
|
||
double x, double y, double z,
|
||
float yaw, float pitch) {
|
||
if (!useMySQL) return;
|
||
|
||
String sql = """
|
||
INSERT INTO ticket_pending_teleport
|
||
(player_uuid, world, x, y, z, yaw, pitch)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE
|
||
world = VALUES(world), x = VALUES(x), y = VALUES(y),
|
||
z = VALUES(z), yaw = VALUES(yaw), pitch = VALUES(pitch),
|
||
created_at = CURRENT_TIMESTAMP
|
||
""";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, playerUUID.toString());
|
||
ps.setString(2, world);
|
||
ps.setDouble(3, x);
|
||
ps.setDouble(4, y);
|
||
ps.setDouble(5, z);
|
||
ps.setFloat(6, yaw);
|
||
ps.setFloat(7, pitch);
|
||
ps.executeUpdate();
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei setPendingTeleport: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Liest und löscht einen ausstehenden Teleport-Auftrag in einem Schritt.
|
||
* Gibt null zurück wenn kein Auftrag vorhanden ist.
|
||
*
|
||
* Rückgabe: double[] { x, y, z, yaw, pitch } + world als Index 0 im String-Array
|
||
* Vereinfacht: gibt ein Object[] zurück: [String world, double x, y, z, float yaw, pitch]
|
||
*/
|
||
public PendingTeleport consumePendingTeleport(UUID playerUUID) {
|
||
if (!useMySQL) return null;
|
||
|
||
String selectSql = "SELECT * FROM ticket_pending_teleport WHERE player_uuid = ?";
|
||
String deleteSql = "DELETE FROM ticket_pending_teleport WHERE player_uuid = ?";
|
||
|
||
try (Connection conn = getConnection();
|
||
PreparedStatement sel = conn.prepareStatement(selectSql)) {
|
||
sel.setString(1, playerUUID.toString());
|
||
ResultSet rs = sel.executeQuery();
|
||
if (!rs.next()) return null;
|
||
|
||
PendingTeleport pt = new PendingTeleport(
|
||
rs.getString("world"),
|
||
rs.getDouble("x"),
|
||
rs.getDouble("y"),
|
||
rs.getDouble("z"),
|
||
rs.getFloat("yaw"),
|
||
rs.getFloat("pitch")
|
||
);
|
||
|
||
// Sofort löschen damit kein zweites Mal teleportiert wird
|
||
try (PreparedStatement del = conn.prepareStatement(deleteSql)) {
|
||
del.setString(1, playerUUID.toString());
|
||
del.executeUpdate();
|
||
}
|
||
return pt;
|
||
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei consumePendingTeleport: " + e.getMessage(), e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** Einfaches Daten-Objekt für einen ausstehenden Teleport-Auftrag. */
|
||
public record PendingTeleport(String world, double x, double y, double z, float yaw, float pitch) {}
|
||
|
||
|
||
|
||
public boolean addComment(TicketComment comment) {
|
||
if (useMySQL) {
|
||
String sql = """
|
||
INSERT INTO ticket_comments (ticket_id, author_uuid, author_name, message)
|
||
VALUES (?, ?, ?, ?)
|
||
""";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setInt(1, comment.getTicketId());
|
||
ps.setString(2, comment.getAuthorUUID().toString());
|
||
ps.setString(3, comment.getAuthorName());
|
||
ps.setString(4, comment.getMessage());
|
||
return ps.executeUpdate() > 0;
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Speichern des Kommentars: " + e.getMessage(), e);
|
||
}
|
||
return false;
|
||
} else {
|
||
int index = dataConfig.getInt("comments." + comment.getTicketId() + ".count", 0);
|
||
String base = "comments." + comment.getTicketId() + "." + index + ".";
|
||
dataConfig.set(base + "authorUUID", comment.getAuthorUUID().toString());
|
||
dataConfig.set(base + "authorName", comment.getAuthorName());
|
||
dataConfig.set(base + "message", comment.getMessage());
|
||
dataConfig.set(base + "createdAt", comment.getCreatedAt() != null ? comment.getCreatedAt().getTime() : System.currentTimeMillis());
|
||
dataConfig.set("comments." + comment.getTicketId() + ".count", index + 1);
|
||
saveDataConfig();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public List<TicketComment> getComments(int ticketId) {
|
||
List<TicketComment> list = new ArrayList<>();
|
||
if (useMySQL) {
|
||
String sql = "SELECT * FROM ticket_comments WHERE ticket_id = ? ORDER BY created_at ASC";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setInt(1, ticketId);
|
||
ResultSet rs = ps.executeQuery();
|
||
while (rs.next()) {
|
||
TicketComment c = new TicketComment();
|
||
c.setId(rs.getInt("id"));
|
||
c.setTicketId(rs.getInt("ticket_id"));
|
||
c.setAuthorUUID(UUID.fromString(rs.getString("author_uuid")));
|
||
c.setAuthorName(rs.getString("author_name"));
|
||
c.setMessage(rs.getString("message"));
|
||
c.setCreatedAt(rs.getTimestamp("created_at"));
|
||
list.add(c);
|
||
}
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Laden der Kommentare: " + e.getMessage(), e);
|
||
}
|
||
} else {
|
||
if (!dataConfig.contains("comments." + ticketId)) return list;
|
||
int count = dataConfig.getInt("comments." + ticketId + ".count", 0);
|
||
for (int i = 0; i < count; i++) {
|
||
String base = "comments." + ticketId + "." + i + ".";
|
||
if (!dataConfig.contains(base + "message")) continue;
|
||
TicketComment c = new TicketComment();
|
||
c.setTicketId(ticketId);
|
||
c.setAuthorUUID(UUID.fromString(dataConfig.getString(base + "authorUUID")));
|
||
c.setAuthorName(dataConfig.getString(base + "authorName"));
|
||
c.setMessage(dataConfig.getString(base + "message"));
|
||
long ts = dataConfig.getLong(base + "createdAt", System.currentTimeMillis());
|
||
c.setCreatedAt(new Timestamp(ts));
|
||
list.add(c);
|
||
}
|
||
}
|
||
return list;
|
||
}
|
||
|
||
// ─────────────────────────── Pending Notifications ─────────────────────
|
||
|
||
public void addPendingNotification(UUID playerUUID, String rawMessage) {
|
||
if (useMySQL) {
|
||
String sql = "INSERT INTO ticket_pending_notifications (player_uuid, message) VALUES (?, ?)";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, playerUUID.toString());
|
||
ps.setString(2, rawMessage);
|
||
ps.executeUpdate();
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Speichern der Pending-Notification: " + e.getMessage(), e);
|
||
}
|
||
} else {
|
||
String path = "pending_notifications." + playerUUID;
|
||
List<String> existing = dataConfig.getStringList(path);
|
||
existing.add(rawMessage);
|
||
dataConfig.set(path, existing);
|
||
saveDataConfig();
|
||
}
|
||
}
|
||
|
||
public List<String> getPendingNotifications(UUID playerUUID) {
|
||
List<String> messages = new ArrayList<>();
|
||
if (useMySQL) {
|
||
String sql = "SELECT message FROM ticket_pending_notifications WHERE player_uuid = ? ORDER BY created_at ASC";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, playerUUID.toString());
|
||
ResultSet rs = ps.executeQuery();
|
||
while (rs.next()) messages.add(rs.getString("message"));
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Laden der Pending-Notifications: " + e.getMessage(), e);
|
||
}
|
||
} else {
|
||
messages = dataConfig.getStringList("pending_notifications." + playerUUID);
|
||
}
|
||
return messages;
|
||
}
|
||
|
||
public void clearPendingNotifications(UUID playerUUID) {
|
||
if (useMySQL) {
|
||
String sql = "DELETE FROM ticket_pending_notifications WHERE player_uuid = ?";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, playerUUID.toString());
|
||
ps.executeUpdate();
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Löschen der Pending-Notifications: " + e.getMessage(), e);
|
||
}
|
||
} else {
|
||
dataConfig.set("pending_notifications." + playerUUID, null);
|
||
saveDataConfig();
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────── Blacklist ─────────────────────────────────
|
||
|
||
public boolean isBlacklisted(UUID uuid) {
|
||
if (useMySQL) {
|
||
String sql = "SELECT COUNT(*) FROM ticket_blacklist WHERE uuid = ?";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, uuid.toString());
|
||
ResultSet rs = ps.executeQuery();
|
||
return rs.next() && rs.getInt(1) > 0;
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei isBlacklisted: " + e.getMessage(), e);
|
||
}
|
||
return false;
|
||
} else {
|
||
return dataConfig.contains("blacklist." + uuid.toString());
|
||
}
|
||
}
|
||
|
||
public boolean addBlacklist(UUID uuid, String playerName, String reason, String bannedBy) {
|
||
if (useMySQL) {
|
||
String sql = "INSERT IGNORE INTO ticket_blacklist (uuid, player_name, reason, banned_by) VALUES (?, ?, ?, ?)";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, uuid.toString());
|
||
ps.setString(2, playerName);
|
||
ps.setString(3, reason != null ? reason : "");
|
||
ps.setString(4, bannedBy);
|
||
return ps.executeUpdate() > 0;
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei addBlacklist: " + e.getMessage(), e);
|
||
}
|
||
return false;
|
||
} else {
|
||
String base = "blacklist." + uuid.toString() + ".";
|
||
dataConfig.set(base + "playerName", playerName);
|
||
dataConfig.set(base + "reason", reason != null ? reason : "");
|
||
dataConfig.set(base + "bannedBy", bannedBy);
|
||
dataConfig.set(base + "bannedAt", System.currentTimeMillis());
|
||
saveDataConfig();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public boolean removeBlacklist(UUID uuid) {
|
||
if (useMySQL) {
|
||
String sql = "DELETE FROM ticket_blacklist WHERE uuid = ?";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, uuid.toString());
|
||
return ps.executeUpdate() > 0;
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei removeBlacklist: " + e.getMessage(), e);
|
||
}
|
||
return false;
|
||
} else {
|
||
if (!dataConfig.contains("blacklist." + uuid.toString())) return false;
|
||
dataConfig.set("blacklist." + uuid.toString(), null);
|
||
saveDataConfig();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public List<String[]> getBlacklist() {
|
||
List<String[]> list = new ArrayList<>();
|
||
if (useMySQL) {
|
||
String sql = "SELECT uuid, player_name, reason, banned_by, banned_at FROM ticket_blacklist ORDER BY banned_at DESC";
|
||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||
ResultSet rs = stmt.executeQuery(sql);
|
||
while (rs.next()) {
|
||
list.add(new String[]{
|
||
rs.getString("uuid"),
|
||
rs.getString("player_name"),
|
||
rs.getString("reason"),
|
||
rs.getString("banned_by"),
|
||
rs.getTimestamp("banned_at").toString()
|
||
});
|
||
}
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler bei getBlacklist: " + e.getMessage(), e);
|
||
}
|
||
} else {
|
||
if (!dataConfig.contains("blacklist")) return list;
|
||
for (String uuid : dataConfig.getConfigurationSection("blacklist").getKeys(false)) {
|
||
String base = "blacklist." + uuid + ".";
|
||
list.add(new String[]{
|
||
uuid,
|
||
dataConfig.getString(base + "playerName", "?"),
|
||
dataConfig.getString(base + "reason", ""),
|
||
dataConfig.getString(base + "bannedBy", "?"),
|
||
String.valueOf(dataConfig.getLong(base + "bannedAt", 0))
|
||
});
|
||
}
|
||
}
|
||
return list;
|
||
}
|
||
|
||
// ─────────────────────────── Abfragen ──────────────────────────────────
|
||
|
||
public List<Ticket> getTicketsByStatus(TicketStatus... statuses) {
|
||
List<Ticket> list = new ArrayList<>();
|
||
if (statuses.length == 0) return list;
|
||
if (useMySQL) {
|
||
StringBuilder ph = new StringBuilder("?");
|
||
for (int i = 1; i < statuses.length; i++) ph.append(",?");
|
||
String sql = "SELECT * FROM tickets WHERE status IN (" + ph + ") ORDER BY created_at ASC";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
for (int i = 0; i < statuses.length; i++) ps.setString(i + 1, statuses[i].name());
|
||
ResultSet rs = ps.executeQuery();
|
||
while (rs.next()) list.add(mapRow(rs));
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Abrufen der Tickets: " + e.getMessage(), e);
|
||
}
|
||
return list;
|
||
} else {
|
||
if (!dataConfig.contains("tickets")) return list;
|
||
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
|
||
Ticket t = (Ticket) dataConfig.get("tickets." + key);
|
||
for (TicketStatus status : statuses) {
|
||
if (t != null && t.getStatus() == status) { list.add(t); break; }
|
||
}
|
||
}
|
||
return list;
|
||
}
|
||
}
|
||
|
||
public List<Ticket> getAllTickets() {
|
||
List<Ticket> list = new ArrayList<>();
|
||
if (useMySQL) {
|
||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||
ResultSet rs = stmt.executeQuery("SELECT * FROM tickets");
|
||
while (rs.next()) list.add(mapRow(rs));
|
||
} catch (SQLException e) {
|
||
sendError("Fehler beim Abrufen aller Tickets: " + e.getMessage());
|
||
}
|
||
} else {
|
||
if (!dataConfig.contains("tickets")) return list;
|
||
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
|
||
Ticket t = (Ticket) dataConfig.get("tickets." + key);
|
||
if (t != null) list.add(t);
|
||
}
|
||
}
|
||
return list;
|
||
}
|
||
|
||
public Ticket getTicketById(int id) {
|
||
if (useMySQL) {
|
||
String sql = "SELECT * FROM tickets WHERE id = ?";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setInt(1, id);
|
||
ResultSet rs = ps.executeQuery();
|
||
if (rs.next()) return mapRow(rs);
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Abrufen des Tickets: " + e.getMessage(), e);
|
||
}
|
||
return null;
|
||
} else {
|
||
if (dataConfig.contains("tickets." + id)) return (Ticket) dataConfig.get("tickets." + id);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public int countOpenTickets() {
|
||
if (useMySQL) {
|
||
String sql = "SELECT COUNT(*) FROM tickets WHERE status = 'OPEN'";
|
||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||
ResultSet rs = stmt.executeQuery(sql);
|
||
if (rs.next()) return rs.getInt(1);
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Zählen der Tickets: " + e.getMessage(), e);
|
||
}
|
||
return 0;
|
||
} else {
|
||
int count = 0;
|
||
if (dataConfig.contains("tickets")) {
|
||
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
|
||
Ticket t = (Ticket) dataConfig.get("tickets." + key);
|
||
if (t != null && t.getStatus() == TicketStatus.OPEN) count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
}
|
||
|
||
public int countOpenTicketsByPlayer(UUID uuid) {
|
||
if (useMySQL) {
|
||
String sql = "SELECT COUNT(*) FROM tickets WHERE creator_uuid = ? AND status IN ('OPEN', 'CLAIMED', 'FORWARDED')";
|
||
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
|
||
ps.setString(1, uuid.toString());
|
||
ResultSet rs = ps.executeQuery();
|
||
if (rs.next()) return rs.getInt(1);
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler: " + e.getMessage(), e);
|
||
}
|
||
return 0;
|
||
} else {
|
||
int count = 0;
|
||
if (dataConfig.contains("tickets")) {
|
||
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
|
||
Ticket t = (Ticket) dataConfig.get("tickets." + key);
|
||
if (t != null && uuid.equals(t.getCreatorUUID())
|
||
&& (t.getStatus() == TicketStatus.OPEN
|
||
|| t.getStatus() == TicketStatus.CLAIMED
|
||
|| t.getStatus() == TicketStatus.FORWARDED)) count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────── Archivierung ──────────────────────────────
|
||
|
||
public int archiveClosedTickets() {
|
||
List<Ticket> all = getAllTickets();
|
||
List<Ticket> toArchive = new ArrayList<>();
|
||
for (Ticket t : all) { if (t.getStatus() == TicketStatus.CLOSED) toArchive.add(t); }
|
||
if (toArchive.isEmpty()) return 0;
|
||
|
||
File archiveFile = new File(plugin.getDataFolder(), archiveFileName);
|
||
JSONArray arr = new JSONArray();
|
||
if (archiveFile.exists()) {
|
||
try (FileReader fr = new FileReader(archiveFile)) {
|
||
Object parsed = new JSONParser().parse(fr);
|
||
if (parsed instanceof JSONArray oldArr) arr.addAll(oldArr);
|
||
} catch (Exception ignored) {}
|
||
}
|
||
for (Ticket t : toArchive) arr.add(ticketToJson(t));
|
||
try (FileWriter fw = new FileWriter(archiveFile)) {
|
||
fw.write(arr.toJSONString());
|
||
} catch (Exception e) { sendError("Fehler beim Archivieren: " + e.getMessage()); return 0; }
|
||
|
||
int removed = 0;
|
||
if (useMySQL) {
|
||
try (Connection conn = getConnection();
|
||
PreparedStatement ps = conn.prepareStatement("DELETE FROM tickets WHERE id = ?")) {
|
||
for (Ticket t : toArchive) { ps.setInt(1, t.getId()); ps.executeUpdate(); removed++; }
|
||
} catch (Exception e) { sendError("Fehler beim Entfernen archivierter Tickets: " + e.getMessage()); }
|
||
} else {
|
||
for (Ticket t : toArchive) { dataConfig.set("tickets." + t.getId(), null); removed++; }
|
||
try { dataConfig.save(dataFile); } catch (Exception e) { sendError("Fehler beim Speichern nach Archivierung: " + e.getMessage()); }
|
||
}
|
||
return removed;
|
||
}
|
||
|
||
// ─────────────────────────── Statistiken ───────────────────────────────
|
||
|
||
public TicketStats getTicketStats() {
|
||
// Aktuelle Live-Daten aus der tickets-Tabelle
|
||
List<Ticket> all = getAllTickets();
|
||
int open = 0, claimed = 0, forwarded = 0, closedLive = 0;
|
||
java.util.Map<String, Integer> byPlayer = new java.util.HashMap<>();
|
||
java.util.Map<String, Integer> byServer = new java.util.HashMap<>();
|
||
for (Ticket t : all) {
|
||
switch (t.getStatus()) {
|
||
case OPEN -> open++;
|
||
case CLAIMED -> claimed++;
|
||
case FORWARDED -> forwarded++;
|
||
case CLOSED -> closedLive++;
|
||
}
|
||
byPlayer.merge(t.getCreatorName(), 1, Integer::sum);
|
||
byServer.merge(t.getServerName(), 1, Integer::sum);
|
||
}
|
||
|
||
// Historische Bewertungen aus der persistenten Stats-Tabelle lesen
|
||
// (enthält auch Daten von bereits gelöschten/archivierten Tickets)
|
||
int thumbsUp = 0, thumbsDown = 0, totalClosedEver = closedLive;
|
||
if (useMySQL) {
|
||
String sql = """
|
||
SELECT
|
||
COUNT(*) AS total,
|
||
SUM(player_rating = 'THUMBS_UP') AS up,
|
||
SUM(player_rating = 'THUMBS_DOWN') AS down
|
||
FROM ticket_stats
|
||
""";
|
||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||
ResultSet rs = stmt.executeQuery(sql);
|
||
if (rs.next()) {
|
||
totalClosedEver = rs.getInt("total");
|
||
thumbsUp = rs.getInt("up");
|
||
thumbsDown = rs.getInt("down");
|
||
}
|
||
} catch (SQLException e) {
|
||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Laden der persistenten Stats: " + e.getMessage(), e);
|
||
// Fallback: Bewertungen aus Live-Daten lesen
|
||
for (Ticket t : all) {
|
||
if ("THUMBS_UP".equals(t.getPlayerRating())) thumbsUp++;
|
||
if ("THUMBS_DOWN".equals(t.getPlayerRating())) thumbsDown++;
|
||
}
|
||
}
|
||
} else {
|
||
// Datei-Modus: nur Live-Daten verfügbar
|
||
for (Ticket t : all) {
|
||
if ("THUMBS_UP".equals(t.getPlayerRating())) thumbsUp++;
|
||
if ("THUMBS_DOWN".equals(t.getPlayerRating())) thumbsDown++;
|
||
}
|
||
}
|
||
|
||
return new TicketStats(all.size(), open, totalClosedEver, forwarded, thumbsUp, thumbsDown, byPlayer, byServer);
|
||
}
|
||
|
||
public static class TicketStats {
|
||
public final int total, open, closed, forwarded, thumbsUp, thumbsDown;
|
||
public final java.util.Map<String, Integer> byPlayer;
|
||
/** BungeeCord: Anzahl Tickets pro Server */
|
||
public final java.util.Map<String, Integer> byServer;
|
||
|
||
public TicketStats(int total, int open, int closed, int forwarded,
|
||
int thumbsUp, int thumbsDown,
|
||
java.util.Map<String, Integer> byPlayer,
|
||
java.util.Map<String, Integer> byServer) {
|
||
this.total = total;
|
||
this.open = open;
|
||
this.closed = closed;
|
||
this.forwarded = forwarded;
|
||
this.thumbsUp = thumbsUp;
|
||
this.thumbsDown = thumbsDown;
|
||
this.byPlayer = byPlayer;
|
||
this.byServer = byServer;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────── Export / Import ───────────────────────────
|
||
|
||
public int exportTickets(File exportFile) {
|
||
List<Ticket> tickets = getAllTickets();
|
||
JSONArray arr = new JSONArray();
|
||
for (Ticket t : tickets) arr.add(ticketToJson(t));
|
||
try (FileWriter fw = new FileWriter(exportFile)) { fw.write(arr.toJSONString()); return tickets.size(); }
|
||
catch (IOException e) { sendError("Fehler beim Export: " + e.getMessage()); return 0; }
|
||
}
|
||
|
||
public int importTickets(File importFile) {
|
||
int imported = 0;
|
||
try (FileReader fr = new FileReader(importFile)) {
|
||
JSONArray arr = (JSONArray) new JSONParser().parse(fr);
|
||
for (Object o : arr) {
|
||
Ticket t = ticketFromJson((JSONObject) o);
|
||
if (t != null && createTicket(t) != -1) imported++;
|
||
}
|
||
} catch (Exception e) { sendError("Fehler beim Import: " + e.getMessage()); }
|
||
return imported;
|
||
}
|
||
|
||
// ─────────────────────────── Migration ─────────────────────────────────
|
||
|
||
public int migrateToMySQL() {
|
||
if (useMySQL || dataConfig == null) return 0;
|
||
int migrated = 0;
|
||
try {
|
||
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
|
||
Ticket t = (Ticket) dataConfig.get("tickets." + key);
|
||
if (t != null) { useMySQL = true; int id = createTicket(t); useMySQL = false; if (id != -1) migrated++; }
|
||
}
|
||
} catch (Exception e) { plugin.getLogger().severe("Fehler bei Migration zu MySQL: " + e.getMessage()); }
|
||
return migrated;
|
||
}
|
||
|
||
public int migrateToFile() {
|
||
if (!useMySQL) return 0;
|
||
int migrated = 0;
|
||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||
ResultSet rs = stmt.executeQuery("SELECT * FROM tickets");
|
||
while (rs.next()) {
|
||
Ticket t = mapRow(rs);
|
||
if (t != null) { useMySQL = false; int id = createTicket(t); useMySQL = true; if (id != -1) migrated++; }
|
||
}
|
||
} catch (Exception e) { plugin.getLogger().severe("Fehler bei Migration zu Datei: " + e.getMessage()); }
|
||
return migrated;
|
||
}
|
||
|
||
// ─────────────────────────── Mapping ───────────────────────────────────
|
||
|
||
private Ticket mapRow(ResultSet rs) throws SQLException {
|
||
Ticket t = new Ticket();
|
||
t.setId(rs.getInt("id"));
|
||
t.setCreatorUUID(UUID.fromString(rs.getString("creator_uuid")));
|
||
t.setCreatorName(rs.getString("creator_name"));
|
||
t.setMessage(rs.getString("message"));
|
||
t.setWorldName(rs.getString("world"));
|
||
t.setX(rs.getDouble("x")); t.setY(rs.getDouble("y")); t.setZ(rs.getDouble("z"));
|
||
t.setYaw(rs.getFloat("yaw")); t.setPitch(rs.getFloat("pitch"));
|
||
t.setStatus(TicketStatus.valueOf(rs.getString("status")));
|
||
t.setCreatedAt(rs.getTimestamp("created_at"));
|
||
t.setClaimedAt(rs.getTimestamp("claimed_at"));
|
||
t.setClosedAt(rs.getTimestamp("closed_at"));
|
||
|
||
safeReadColumn(rs, "close_comment", v -> t.setCloseComment(v));
|
||
safeReadColumn(rs, "claimer_uuid", v -> { t.setClaimerUUID(UUID.fromString(v)); });
|
||
safeReadColumn(rs, "claimer_name", v -> t.setClaimerName(v));
|
||
safeReadColumn(rs, "forwarded_to_uuid",v -> t.setForwardedToUUID(UUID.fromString(v)));
|
||
safeReadColumn(rs, "forwarded_to_name",v -> t.setForwardedToName(v));
|
||
|
||
try { t.setPlayerDeleted(rs.getBoolean("player_deleted")); } catch (SQLException ignored) {}
|
||
try { t.setCategoryKey(rs.getString("category")); } catch (SQLException ignored) {}
|
||
try { t.setPriority(TicketPriority.fromString(rs.getString("priority"))); } catch (SQLException ignored) {}
|
||
try { t.setPlayerRating(rs.getString("player_rating")); } catch (SQLException ignored) {}
|
||
try { t.setClaimerNotified(rs.getBoolean("claimer_notified")); } catch (SQLException ignored) {}
|
||
// Bug-Fix: close_notified für duplikat-freie Schließ-Benachrichtigungen
|
||
try { t.setCloseNotified(rs.getBoolean("close_notified")); } catch (SQLException ignored) {}
|
||
// BungeeCord: Server-Name einlesen
|
||
try { t.setServerName(rs.getString("server_name")); } catch (SQLException ignored) {}
|
||
return t;
|
||
}
|
||
|
||
@FunctionalInterface private interface StringConsumer { void accept(String s); }
|
||
private void safeReadColumn(ResultSet rs, String col, StringConsumer consumer) {
|
||
try { String v = rs.getString(col); if (v != null) consumer.accept(v); } catch (SQLException ignored) {}
|
||
}
|
||
|
||
// ─────────────────────────── JSON-Hilfsmethoden ─────────────────────────
|
||
|
||
@SuppressWarnings("unchecked")
|
||
private JSONObject ticketToJson(Ticket t) {
|
||
JSONObject obj = new JSONObject();
|
||
obj.put("id", t.getId());
|
||
obj.put("creatorUUID", t.getCreatorUUID().toString());
|
||
obj.put("creatorName", t.getCreatorName());
|
||
obj.put("message", t.getMessage());
|
||
obj.put("world", t.getWorldName());
|
||
obj.put("x", t.getX()); obj.put("y", t.getY()); obj.put("z", t.getZ());
|
||
obj.put("yaw", t.getYaw()); obj.put("pitch", t.getPitch());
|
||
obj.put("status", t.getStatus().name());
|
||
obj.put("createdAt", t.getCreatedAt() != null ? t.getCreatedAt().getTime() : null);
|
||
obj.put("claimedAt", t.getClaimedAt() != null ? t.getClaimedAt().getTime() : null);
|
||
obj.put("closedAt", t.getClosedAt() != null ? t.getClosedAt().getTime() : null);
|
||
if (t.getClaimerUUID() != null) obj.put("claimerUUID", t.getClaimerUUID().toString());
|
||
if (t.getClaimerName() != null) obj.put("claimerName", t.getClaimerName());
|
||
if (t.getForwardedToUUID() != null) obj.put("forwardedToUUID", t.getForwardedToUUID().toString());
|
||
if (t.getForwardedToName() != null) obj.put("forwardedToName", t.getForwardedToName());
|
||
if (t.getCloseComment() != null) obj.put("closeComment", t.getCloseComment());
|
||
obj.put("playerDeleted", t.isPlayerDeleted());
|
||
obj.put("category", t.getCategoryKey());
|
||
obj.put("priority", t.getPriority().name());
|
||
if (t.getPlayerRating() != null) obj.put("playerRating", t.getPlayerRating());
|
||
obj.put("claimerNotified", t.isClaimerNotified());
|
||
obj.put("closeNotified", t.isCloseNotified());
|
||
// BungeeCord: Server-Name im JSON-Export
|
||
obj.put("serverName", t.getServerName());
|
||
return obj;
|
||
}
|
||
|
||
private Ticket ticketFromJson(JSONObject obj) {
|
||
try {
|
||
Ticket t = new Ticket();
|
||
t.setId(((Long) obj.get("id")).intValue());
|
||
t.setCreatorUUID(UUID.fromString((String) obj.get("creatorUUID")));
|
||
t.setCreatorName((String) obj.get("creatorName"));
|
||
t.setMessage((String) obj.get("message"));
|
||
t.setWorldName((String) obj.get("world"));
|
||
t.setX((Double) obj.get("x")); t.setY((Double) obj.get("y")); t.setZ((Double) obj.get("z"));
|
||
t.setYaw(((Double) obj.get("yaw")).floatValue());
|
||
t.setPitch(((Double) obj.get("pitch")).floatValue());
|
||
t.setStatus(TicketStatus.valueOf((String) obj.get("status")));
|
||
if (obj.get("createdAt") != null) t.setCreatedAt(new Timestamp((Long) obj.get("createdAt")));
|
||
if (obj.get("claimedAt") != null) t.setClaimedAt(new Timestamp((Long) obj.get("claimedAt")));
|
||
if (obj.get("closedAt") != null) t.setClosedAt(new Timestamp((Long) obj.get("closedAt")));
|
||
if (obj.get("claimerUUID") != null) t.setClaimerUUID(UUID.fromString((String) obj.get("claimerUUID")));
|
||
if (obj.get("claimerName") != null) t.setClaimerName((String) obj.get("claimerName"));
|
||
if (obj.get("forwardedToUUID")!= null) t.setForwardedToUUID(UUID.fromString((String) obj.get("forwardedToUUID")));
|
||
if (obj.get("forwardedToName")!= null) t.setForwardedToName((String) obj.get("forwardedToName"));
|
||
if (obj.get("closeComment") != null) t.setCloseComment((String) obj.get("closeComment"));
|
||
if (obj.containsKey("playerDeleted")) t.setPlayerDeleted((Boolean) obj.get("playerDeleted"));
|
||
if (obj.containsKey("category")) t.setCategoryKey((String) obj.get("category"));
|
||
if (obj.containsKey("priority")) t.setPriority(TicketPriority.fromString((String) obj.get("priority")));
|
||
if (obj.containsKey("playerRating")) t.setPlayerRating((String) obj.get("playerRating"));
|
||
if (obj.containsKey("claimerNotified"))t.setClaimerNotified((Boolean) obj.get("claimerNotified"));
|
||
if (obj.containsKey("closeNotified")) t.setCloseNotified((Boolean) obj.get("closeNotified"));
|
||
// BungeeCord: Server-Name aus JSON
|
||
if (obj.containsKey("serverName")) t.setServerName((String) obj.get("serverName"));
|
||
return t;
|
||
} catch (Exception e) {
|
||
if (plugin != null) plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage());
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────── Validierung ───────────────────────────────
|
||
|
||
private void validateLoadedTickets() {
|
||
if (dataConfig == null || !dataConfig.contains("tickets")) return;
|
||
int invalid = 0;
|
||
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
|
||
Object obj = dataConfig.get("tickets." + key);
|
||
if (!(obj instanceof Ticket t)) { sendError("Ungültiges Ticket-Objekt bei ID: " + key); invalid++; continue; }
|
||
if (t.getCreatorUUID() == null || t.getCreatorName() == null || t.getMessage() == null || t.getStatus() == null) {
|
||
sendError("Ticket mit fehlenden Pflichtfeldern: ID " + key); invalid++;
|
||
}
|
||
}
|
||
if (invalid > 0) {
|
||
String msg = plugin != null
|
||
? plugin.formatMessage("messages.validation-warning").replace("{count}", String.valueOf(invalid))
|
||
: invalid + " ungültige Tickets beim Laden gefunden.";
|
||
sendError(msg);
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────── Persistenz-Helper ─────────────────────────
|
||
|
||
private void saveDataConfig() {
|
||
try { dataConfig.save(dataFile); }
|
||
catch (IOException e) { if (plugin != null) plugin.getLogger().severe("Fehler beim Speichern von " + dataFileName + ": " + e.getMessage()); }
|
||
}
|
||
|
||
// ─────────────────────────── Backup (Platzhalter) ──────────────────────
|
||
private void backupMySQL() {}
|
||
private void backupDataFile() {}
|
||
} |