package com.craftbank.database; import com.craftbank.CraftBank; import com.craftbank.model.PlayerAccount; import com.craftbank.model.TermDeposit; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.bukkit.configuration.file.FileConfiguration; import java.io.File; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.logging.Level; /** * 数据库管理器。支持 SQLite (默认) 与 MySQL, 通过 HikariCP 管理连接池。 * *
所有方法均为阻塞式 JDBC 调用, 必须 在 Folia 的 AsyncScheduler * 线程上调用, 严禁在主区域线程同步调用。
*/ public class DatabaseManager { public enum Type { SQLITE, MYSQL } private final CraftBank plugin; private HikariDataSource dataSource; private Type type; public DatabaseManager(CraftBank plugin) { this.plugin = plugin; } public void init() throws SQLException { FileConfiguration cfg = plugin.getConfig(); String typeName = cfg.getString("Database.type", "SQLITE").toUpperCase(); this.type = "MYSQL".equals(typeName) ? Type.MYSQL : Type.SQLITE; HikariConfig hikari = new HikariConfig(); hikari.setPoolName("CraftBank-Pool"); if (type == Type.MYSQL) { String host = cfg.getString("Database.mysql.host", "localhost"); int port = cfg.getInt("Database.mysql.port", 3306); String db = cfg.getString("Database.mysql.database", "craftbank"); boolean ssl = cfg.getBoolean("Database.mysql.useSSL", false); hikari.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + db + "?useSSL=" + ssl + "&useUnicode=true&characterEncoding=utf8&autoReconnect=true"); hikari.setUsername(cfg.getString("Database.mysql.username", "root")); hikari.setPassword(cfg.getString("Database.mysql.password", "")); hikari.setMaximumPoolSize(cfg.getInt("Database.mysql.pool-size", 10)); } else { File file = new File(plugin.getDataFolder(), cfg.getString("Database.sqlite-file", "craftbank.db")); hikari.setDriverClassName("org.sqlite.JDBC"); hikari.setJdbcUrl("jdbc:sqlite:" + file.getAbsolutePath()); // SQLite 单写入者, 限制为 1 条连接以避免 "database is locked"。 hikari.setMaximumPoolSize(1); } hikari.setConnectionTimeout(10000); this.dataSource = new HikariDataSource(hikari); createTables(); } private void createTables() throws SQLException { String autoInc = type == Type.MYSQL ? "INT AUTO_INCREMENT PRIMARY KEY" : "INTEGER PRIMARY KEY AUTOINCREMENT"; String players = "CREATE TABLE IF NOT EXISTS craftbank_players (" + "uuid VARCHAR(36) PRIMARY KEY," + "player_name VARCHAR(32)," + "cash DOUBLE NOT NULL DEFAULT 0," + "bank_saving DOUBLE NOT NULL DEFAULT 0," + "last_interest BIGINT NOT NULL DEFAULT 0," + "card_serial BIGINT NOT NULL DEFAULT 0" + ")"; String deposits = "CREATE TABLE IF NOT EXISTS craftbank_term_deposits (" + "id " + autoInc + "," + "owner VARCHAR(36) NOT NULL," + "principal DOUBLE NOT NULL," + "daily_rate DOUBLE NOT NULL," + "duration_days INT NOT NULL," + "start_time BIGINT NOT NULL," + "end_time BIGINT NOT NULL," + "claimed INT NOT NULL DEFAULT 0" + ")"; try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) { st.executeUpdate(players); st.executeUpdate(deposits); } } public void close() { if (dataSource != null && !dataSource.isClosed()) { dataSource.close(); } } // --------------------------------------------------------------------- // 玩家账户 // --------------------------------------------------------------------- /** 读取账户, 不存在返回 null。 */ public PlayerAccount loadAccount(UUID uuid) { String sql = "SELECT player_name, cash, bank_saving, last_interest, card_serial FROM craftbank_players WHERE uuid = ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, uuid.toString()); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return new PlayerAccount(uuid, rs.getString("player_name"), rs.getDouble("cash"), rs.getDouble("bank_saving"), rs.getLong("last_interest"), rs.getLong("card_serial")); } } } catch (SQLException ex) { plugin.getLogger().log(Level.SEVERE, "读取账户失败: " + uuid, ex); } return null; } public void saveAccount(PlayerAccount account) { String sql = type == Type.MYSQL ? "INSERT INTO craftbank_players (uuid, player_name, cash, bank_saving, last_interest, card_serial) " + "VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE player_name=VALUES(player_name)," + "cash=VALUES(cash), bank_saving=VALUES(bank_saving), last_interest=VALUES(last_interest), card_serial=VALUES(card_serial)" : "INSERT INTO craftbank_players (uuid, player_name, cash, bank_saving, last_interest, card_serial) " + "VALUES (?,?,?,?,?,?) ON CONFLICT(uuid) DO UPDATE SET player_name=excluded.player_name," + "cash=excluded.cash, bank_saving=excluded.bank_saving, last_interest=excluded.last_interest, card_serial=excluded.card_serial"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 在持有快照期间数据可能被其他线程修改, 直接读取同步后的字段。 synchronized (account) { ps.setString(1, account.getUuid().toString()); ps.setString(2, account.getName()); ps.setDouble(3, account.getCash()); ps.setDouble(4, account.getBankSaving()); ps.setLong(5, account.getLastInterestSettle()); ps.setLong(6, account.getCardSerial()); } ps.executeUpdate(); account.setDirty(false); } catch (SQLException ex) { plugin.getLogger().log(Level.SEVERE, "保存账户失败: " + account.getUuid(), ex); } } /** 现金排行榜。 */ public List