Files
TicketSystem/src/main/java/de/ticketsystem/database/DatabaseManager.java
2026-02-21 18:41:15 +01:00

1512 lines
74 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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() {}
}