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 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 ) 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; """; try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { stmt.execute(ticketsSql); stmt.execute(commentsSql); stmt.execute(blacklistSql); stmt.execute(notifSql); } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), e); } } /** * Ergänzt fehlende Spalten in bestehenden Datenbanken automatisch. */ 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"); } 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); } } // ─────────────────────────── CRUD Tickets ────────────────────────────── public int createTicket(Ticket ticket) { if (useMySQL) { String sql = """ INSERT INTO tickets (creator_uuid, creator_name, message, world, x, y, z, yaw, pitch, category, priority) 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.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) return rs.getInt(1); } 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); saveDataConfig(); return id; } } 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 = 'OPEN' """; 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.OPEN) 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; } } // ─────────────────────────── [NEW] Claim-Benachrichtigung markieren ──── /** * Setzt claimer_notified = TRUE für ein Ticket (persistiert in DB/Datei). */ 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(); } } } // ─────────────────────────── [NEW] Bewertung ─────────────────────────── /** * Speichert die Bewertung eines Spielers für sein geschlossenes Ticket. * @param ticketId ID des Tickets * @param rating "THUMBS_UP" oder "THUMBS_DOWN" * @return true bei Erfolg */ 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); return ps.executeUpdate() > 0; } 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; } } // ─────────────────────────── [NEW] Kommentare ────────────────────────── /** * Speichert einen neuen Kommentar/Reply auf ein Ticket. */ 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 { // YAML: comments.. 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; } } /** * Lädt alle Kommentare für ein Ticket, sortiert nach Datum. */ public List getComments(int ticketId) { List 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 ──────────────────── /** * Speichert eine Benachrichtigung für einen offline Spieler. * Wird beim nächsten Login angezeigt. */ 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 existing = dataConfig.getStringList(path); existing.add(rawMessage); dataConfig.set(path, existing); saveDataConfig(); } } /** * Lädt alle ausstehenden Benachrichtigungen für einen Spieler. */ public List getPendingNotifications(UUID playerUUID) { List 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; } /** * Löscht alle ausstehenden Benachrichtigungen eines Spielers nach dem Anzeigen. */ 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(); } } // ─────────────────────────── [NEW] 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; } } /** Gibt alle gesperrten Spieler als Liste von String-Arrays {uuid, name, reason, bannedBy} zurück. */ public List getBlacklist() { List 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 getTicketsByStatus(TicketStatus... statuses) { List 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 getAllTickets() { List 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 all = getAllTickets(); List 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() { List all = getAllTickets(); int open = 0, claimed = 0, forwarded = 0, closed = 0, thumbsUp = 0, thumbsDown = 0; java.util.Map byPlayer = new java.util.HashMap<>(); for (Ticket t : all) { switch (t.getStatus()) { case OPEN -> open++; case CLAIMED -> claimed++; case FORWARDED -> forwarded++; case CLOSED -> closed++; } if ("THUMBS_UP".equals(t.getPlayerRating())) thumbsUp++; if ("THUMBS_DOWN".equals(t.getPlayerRating())) thumbsDown++; byPlayer.merge(t.getCreatorName(), 1, Integer::sum); } return new TicketStats(all.size(), open, closed, forwarded, thumbsUp, thumbsDown, byPlayer); } public static class TicketStats { public final int total, open, closed, forwarded, thumbsUp, thumbsDown; public final java.util.Map byPlayer; public TicketStats(int total, int open, int closed, int forwarded, int thumbsUp, int thumbsDown, java.util.Map byPlayer) { this.total = total; this.open = open; this.closed = closed; this.forwarded = forwarded; this.thumbsUp = thumbsUp; this.thumbsDown = thumbsDown; this.byPlayer = byPlayer; } } // ─────────────────────────── Export / Import ─────────────────────────── public int exportTickets(File exportFile) { List 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) {} 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()); 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")); 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() {} }